<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: realist</title>
    <description>The latest articles on DEV Community by realist (@realist).</description>
    <link>https://dev.to/realist</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4071167%2F7d5a5be3-cd0c-4565-bf42-f38e84604160.png</url>
      <title>DEV Community: realist</title>
      <link>https://dev.to/realist</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/realist"/>
    <language>en</language>
    <item>
      <title>Porting qs to Go: an honest account</title>
      <dc:creator>realist</dc:creator>
      <pubDate>Mon, 10 Aug 2026 19:00:35 +0000</pubDate>
      <link>https://dev.to/realist/porting-qs-to-go-an-honest-account-5e4l</link>
      <guid>https://dev.to/realist/porting-qs-to-go-an-honest-account-5e4l</guid>
      <description>&lt;p&gt;I spent the weekend rewriting &lt;code&gt;qs&lt;/code&gt;, the query-string parser bundled inside Express, used across a huge chunk of the Node ecosystem to Go language. This is what actually happened: what I picked, where my first version was wrong, how I checked that the rewrite behaves the same as the original.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;qs&lt;/code&gt;, why Go
&lt;/h2&gt;

&lt;p&gt;I wanted a library that mattered and a library I could actually finish. &lt;code&gt;qs&lt;/code&gt; fit both: 8.9k stars on github, a real dependency in a lot of production code, and about 1,000 lines of core logic. To port properly in a weekend instead of half-porting something bigger.&lt;/p&gt;

&lt;p&gt;Go was the target for a specific reason, not just because "Go is fast." &lt;code&gt;qs&lt;/code&gt; today only runs inside a Node process ; npm install, node_modules, a JS runtime spinning up to split a query string. A Go port compiles to a single static binary with no runtime and, if I did it right, no external dependencies at all. That last part turned out to matter more than I expected, because it ruled out the easy way to handle one particular edge case (more on that below).&lt;/p&gt;

&lt;p&gt;The hackathon's rule made the whole thing harder in a useful way: the port has to pass the &lt;em&gt;original&lt;/em&gt; test suite, completely unmodified, not a rewritten version of it that matches my own understanding of the spec. &lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the original tests to run against Go at all
&lt;/h2&gt;

&lt;p&gt;JavaScript can't call a Go function directly, so before porting any logic I had to build a bridge between the two. The Go code compiles into a CLI that reads a JSON request off stdin and writes a JSON response to stdout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;request&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;bin/qs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(stdin)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"parse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"a[b]=c"&lt;/span&gt;&lt;span class="p"&gt;,&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 class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;bin/qs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(stdout)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"b"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c"&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To keep the test files themselves untouched, I intercepted Node's module loader so that when the original &lt;a href="https://github.com/ljharb/qs" rel="noopener noreferrer"&gt;&lt;code&gt;ljharb/qs&lt;/code&gt;&lt;/a&gt; tests call &lt;code&gt;require('../')&lt;/code&gt;, they get silently redirected to a small adapter that shells out to the Go binary instead of the real package. The test files never change,  &lt;code&gt;tests/original/&lt;/code&gt; matches the upstream commit (&lt;code&gt;3a890d4&lt;/code&gt;) byte for byte, verifiable with &lt;code&gt;git diff&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This mattered because it's the part that's easiest to fudge under a deadline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually broken
&lt;/h2&gt;

&lt;p&gt;My first working version passed 166 of 241 parse tests. By the end it was 221 of 241 which brings it upto 390 of 410 across the full suite, 95% and almost every point between those two numbers came from a specific, traceable bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalar-into-array merges were structurally wrong.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:     a=b  followed by  a[]=c

expected:  ["b", "c"]                      // qs wraps the scalar, then concatenates
got:       {"0": "b", "1": "c"}            // my merge built an index-keyed object instead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks similar in a JSON dump but is a different data structure. The fix was small once I found it. Detect "target is scalar, source is array" as its own case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// merge.go&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;isScalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But finding it meant realizing the merge code had no concept of what a value used to be before merging, only what it currently looked like.&lt;/p&gt;

&lt;h3&gt;
  
  
  a[0]=b and a[]=c weren't recognized as the same array.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:     a[0]=b  followed by  a[]=c

expected:  {a: ['b', 'c']}
got:       {a: {0: ['c', 'b']}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was parsing bracket-index notation and empty-bracket notation into two separate arrays, then trying to reconcile them during a merge step. By that point the information needed to merge them correctly was already gone. Nine tests failed on this shape before I traced it back to where the real fix belonged: the original resolves this ambiguity during key parsing, before any object gets constructed, not afterward.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;depth: false&lt;/code&gt; was silently ignored.
&lt;/h3&gt;

&lt;p&gt;Not a parsing bug but a bridge bug. &lt;code&gt;qs&lt;/code&gt; treats &lt;code&gt;depth: false&lt;/code&gt; as "no limit," but my CLI's option decoder only checked for a numeric depth field in the incoming JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// before — bool false falls through, depth stays at its default (5)&lt;/span&gt;
&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Depth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One added case fixed it. The lesson: when you port a library across a JSON-serialized boundary, the serialization layer accumulates its own bugs, separate from whatever you're porting.&lt;/p&gt;

&lt;p&gt;Some failures I left as documented limitations rather than forcing a fix.  Eight tests expect a JavaScript &lt;code&gt;TypeError&lt;/code&gt; for something like passing &lt;code&gt;null&lt;/code&gt; where a boolean belongs but &lt;code&gt;JSON.stringify&lt;/code&gt; collapses "explicitly null," "undefined," and "never set" into the same thing once it crosses into JSON, so by the time it reaches Go it's just &lt;code&gt;nil&lt;/code&gt;. Recovering that distinction needs type-checking on the JavaScript side of the bridge, before the JSON is built, a different piece of work than porting &lt;code&gt;qs&lt;/code&gt; itself. One test passes a circular reference into &lt;code&gt;parse()&lt;/code&gt;, which can't be fixed inside this architecture at all: &lt;code&gt;json.Marshal&lt;/code&gt; cannot serialize a cycle, in any language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking equivalence beyond what the tests happened to cover
&lt;/h2&gt;

&lt;p&gt;A test suite only checks the inputs someone thought to write down. To go past that, I ran Go's native fuzzer against a roundtrip property that doesn't depend on any test file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// fuzz/harness_test.go&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;FuzzRoundtrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fuzz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptions&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptions&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultOptions&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;reflect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeepEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"roundtrip mismatch for %q:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  a=%#v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  b=%#v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Across millions of randomly generated inputs; deep nesting, malformed percent-encoding, empty and boundary values. It caught a real divergence within the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;input:      "%80"                      // a lone, invalid continuation byte

parse:      string containing raw byte 0x80        // Go strings can hold arbitrary bytes
stringify:  "%EF%BF%BD"                             // range over the string replaces
                                                      // the invalid byte with U+FFFD first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript never hits this because &lt;code&gt;decodeURIComponent&lt;/code&gt; throws on malformed input before a bad byte can exist as a JS string at all. I documented it rather than trying to patch around it: for binary-safe roundtrips, base64-encode first, the same advice the WHATWG URL spec gives for the same class of problem.&lt;/p&gt;

&lt;p&gt;cold start and memory numbers are real and measured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;realist@realist-MacBook-Pro portmortem-qs % go test -bench=. -benchmem -benchtime=1s ./bench/
goos: darwin
goarch: arm64
pkg: github.com/Len3hq/qs-go/bench
cpu: Apple M1
BenchmarkParse-8                   47172             25417 ns/op           38255 B/op        453 allocs/op
BenchmarkParseSimple-8           1393298               845.4 ns/op          1664 B/op         19 allocs/op
BenchmarkParseNested-8            580536              1964 ns/op            2849 B/op         36 allocs/op
BenchmarkParseArray-8             394047              2965 ns/op            4705 B/op         66 allocs/op
BenchmarkStringify-8              112891             10538 ns/op            4729 B/op        223 allocs/op
BenchmarkStringifySimple-8       5312035               226.2 ns/op            72 B/op          6 allocs/op
BenchmarkStringifyNested-8       1427907               941.0 ns/op           248 B/op         16 allocs/op
BenchmarkStringifyArray-8         770910              1471 ns/op             568 B/op         31 allocs/op
PASS
ok      github.com/Len3hq/qs-go/bench   12.840s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where JS and Go just model things differently
&lt;/h2&gt;

&lt;p&gt;A few divergences weren't bugs at all, they're places where the two languages disagree about what's even representable and the honest move is documenting the gap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;qs&lt;/code&gt; guards against keys named &lt;code&gt;__proto__&lt;/code&gt; because writing to it in JavaScript can reach up and mutate &lt;code&gt;Object.prototype&lt;/code&gt; for the whole program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// why the guard exists in JS&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__proto__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;polluted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;({}).&lt;/span&gt;&lt;span class="nx"&gt;polluted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// true — every object in the program is now affected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go maps have no prototype chain, so the same key is just a string like any other:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"__proto__"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// m["__proto__"] is just a string key. Nothing else in the program is reachable from it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I kept the &lt;code&gt;AllowPrototypes&lt;/code&gt; option for API compatibility, but it's a documented no-op, because the thing it protects against doesn't exist in Go.&lt;/p&gt;

&lt;p&gt;Sparse arrays are the same kind of gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JS:  [, 'b', , 'd']          // holes — positions never assigned anything
Go:  make([]any, 4)          // [nil, nil, nil, nil] — no concept of "never assigned"
                              // vs. "explicitly set to null"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;code&gt;nil&lt;/code&gt; for holes and a separate sentinel type for explicit nulls, which recovers the distinction in most cases, but the original test suite's equality check still treats a JS hole and a Go &lt;code&gt;nil&lt;/code&gt; as different things in a handful of deeply nested cases.&lt;/p&gt;

&lt;p&gt;The ISO-8859-1 charset support was a smaller version of the same tradeoff, but a deliberate one. &lt;code&gt;qs&lt;/code&gt; leans on a quirk of the browser's old &lt;code&gt;unescape()&lt;/code&gt; function for this charset. Go's standard library has no equivalent, and the easy fix &lt;code&gt;go get golang.org/x/text&lt;/code&gt;  was one command away. I didn't take it, because a single external dependency for one option would have undercut the whole pitch of a zero-dependency static binary. I hand-wrote the byte-to-rune mapping table instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Latin-1's first 256 codepoints map directly onto Unicode's first 256.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;latin1ToRune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;rune&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;rune&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Slower to build, and the right call for what the port was supposed to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still unfinished
&lt;/h2&gt;

&lt;p&gt;My CLI currently uses Go's &lt;code&gt;panic()&lt;/code&gt; to reject invalid option types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// current — kills the binary, JS side gets silence instead of a TypeError&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"decodeDotInKeys"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"decodeDotInKeys must be a boolean"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single shortcut accounts for eight of my remaining twenty test failures and isn't a hard fix, it's the one I deprioritized in favor of the merge-logic bugs that felt more urgent at the time. It needs to become a returned error that the adapter turns back into a real &lt;code&gt;TypeError&lt;/code&gt;, not a crash.&lt;/p&gt;

&lt;p&gt;What's shipped now passes 390 of 410 tests from the completely unmodified original suite (95% pass) and every remaining failure traces to something specific: a handful to a real limitation of talking to Go over JSON, the rest to documented differences between how JavaScript and Go represent objects and arrays. Not logic I got wrong and didn't notice, logic I can point to and explain.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/Len3hq/qs-go" rel="noopener noreferrer"&gt;github.com/Len3hq/qs-go&lt;/a&gt; — built for Port Mortem 2026, Track F (JavaScript → Go), hosted by &lt;a href="https://x.com/raptors_hack" rel="noopener noreferrer"&gt;HackathonRaptors&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>go</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
