<?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: Onur Cinar</title>
    <description>The latest articles on DEV Community by Onur Cinar (@onurcinar).</description>
    <link>https://dev.to/onurcinar</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%2F2455507%2F3aa78de4-9412-4988-b03a-d64d419c7f0a.jpeg</url>
      <title>DEV Community: Onur Cinar</title>
      <link>https://dev.to/onurcinar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onurcinar"/>
    <language>en</language>
    <item>
      <title>The Go Validator Blind Spot: Invisible Characters and Unicode Lookalikes</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 06 Sep 2026 16:01:14 +0000</pubDate>
      <link>https://dev.to/onurcinar/the-go-validator-blind-spot-invisible-characters-and-unicode-lookalikes-38j5</link>
      <guid>https://dev.to/onurcinar/the-go-validator-blind-spot-invisible-characters-and-unicode-lookalikes-38j5</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A username that reads "admin" on every screen you check it on can be storing something else entirely -- and it'll sail past a reserved-name check that's doing exactly what it was told. Most Go validation stacks have no way to catch it. Here's why, and two small normalizers that close the gap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's a username: &lt;code&gt;‮nimda‬&lt;/code&gt;. Depending on how your browser renders bidirectional text, that might already display as &lt;code&gt;admin&lt;/code&gt; — which is the entire point. Read it in a user list, a support ticket, or an audit log, and every person who looks at it will tell you the account is named &lt;code&gt;admin&lt;/code&gt;. Now compare it to the literal string, the way a reserved-name check normally works:&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;spoofed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"‮nimda‬"&lt;/span&gt; &lt;span class="c"&gt;// renders as "admin" wherever bidi text is displayed&lt;/span&gt;

&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spoofed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// false&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&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="n"&gt;spoofed&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="c"&gt;// 7, not 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't &lt;code&gt;"admin"&lt;/code&gt;. It's the five letters &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; — in that literal order — wrapped in two invisible bidirectional control characters, U+202E (right-to-left override) and U+202C (pop directional formatting). Unicode reserves those for rendering Arabic and Hebrew correctly. Wrapped around plain Latin letters instead, they just flip the visual order: &lt;code&gt;nimda&lt;/code&gt;, laid out right-to-left, reads &lt;code&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A reserved-username check that rejects the exact string &lt;code&gt;"admin"&lt;/code&gt; lets this straight through — correctly, by its own logic, since the string genuinely isn't &lt;code&gt;"admin"&lt;/code&gt;. Nothing here is a bug in &lt;code&gt;checker&lt;/code&gt;, or in any other Go validation library, or even in Unicode. The check did exactly what it was told. The problem is one level up: it was never asked whether the string secretly contains characters that don't correspond to anything the comparison sees but everyone looking at a screen does.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a new attack
&lt;/h2&gt;

&lt;p&gt;The trick behind that username has a name: in November 2021, researchers at Cambridge published &lt;a href="https://trojansource.codes/" rel="noopener noreferrer"&gt;Trojan Source (CVE-2021-42574)&lt;/a&gt;, showing that the same bidirectional control characters can be slipped into source code to make it &lt;em&gt;compile&lt;/em&gt; one way and &lt;em&gt;display&lt;/em&gt; another way to a human reviewer. The characters are identical; only the target changes. Anything a person reads back instead of a compiler — a username, a chat message, a support ticket, a PR title — is just as susceptible.&lt;/p&gt;

&lt;p&gt;A narrower, older trick uses zero-width characters instead of bidi ones: insert a zero-width space or zero-width joiner into a blocklisted word and a naive filter no longer matches it, even though the word displays unchanged. It's a standard way spam and moderation filters get evaded, and it works against a validation struct tag exactly as well as it works against a regex.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half of the problem: it doesn't need to be invisible at all
&lt;/h2&gt;

&lt;p&gt;Zero-width characters are the sharp case, but plain Unicode gives you the same problem with characters you &lt;em&gt;can&lt;/em&gt; see. &lt;code&gt;"ALICE"&lt;/code&gt;, &lt;code&gt;"ＡＬＩＣＥ"&lt;/code&gt; (fullwidth Latin letters), and a version using ligatures or Roman numerals in place of ordinary letters are three different byte sequences that a human reading them would call the same word. A uniqueness check, a keyword filter, or a duplicate-handle guard that only compares strings byte-for-byte treats all three as unrelated.&lt;/p&gt;

&lt;p&gt;This isn't hypothetical, either — fullwidth-character substitution is a known way to slip a blocked command past a keyword-based filter (&lt;code&gt;ｒｍ　－ｒｆ　／&lt;/code&gt; reads as &lt;code&gt;rm -rf /&lt;/code&gt; to anyone who looks at it, and normalizes right back to it), and several platforms have had to retrofit Unicode normalization after discovering it let people register visually-identical duplicate usernames.&lt;/p&gt;

&lt;p&gt;None of this is because validation is careless. &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;min-len&lt;/code&gt;, an exact-match reserved-name rule — every one of them is a byte-level check, and byte-level checks were never asked a different question: does this string contain characters that don't correspond to anything the person reading it actually typed? That's a normalization problem, not a validation one. It needs its own step in the pipeline, before anything else looks at the value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two checkers, two different problems
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;checker&lt;/code&gt; added two normalizers for this, deliberately kept separate because they solve different problems and have different costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;strip-invisible&lt;/code&gt;&lt;/strong&gt; (core module, no dependency) removes zero-width space, zero-width non-joiner, zero-width joiner, word joiner, the zero-width no-break space (BOM), and the bidirectional embedding/override/isolate controls behind Trojan Source:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Handle&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim strip-invisible required min-len:3"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"adm‌in"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// h.Name is now "admin" -- five runes, matches the literal string.&lt;/span&gt;

&lt;span class="n"&gt;unmasked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StripInvisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"‮nimda‬"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unmasked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c"&gt;// nimda&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unmasked&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// false -- and now it doesn't even display like it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the username from the opening through it and the disguise falls apart: &lt;code&gt;strip-invisible&lt;/code&gt; doesn't know or care what the override characters were trying to accomplish, it just removes them — leaving the honest &lt;code&gt;nimda&lt;/code&gt;, which no longer reads as &lt;code&gt;admin&lt;/code&gt; in any renderer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;nfkc&lt;/code&gt;&lt;/strong&gt; (an opt-in module — it needs &lt;code&gt;golang.org/x/text/unicode/norm&lt;/code&gt;, so it's kept out of the dependency-free core) applies &lt;a href="https://unicode.org/reports/tr15/" rel="noopener noreferrer"&gt;Unicode Normalization Form KC&lt;/a&gt;: it folds compatibility characters — fullwidth forms, ligatures, and similar stylistic variants — into their canonical equivalents:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2/nfkc"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Handle&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim nfkc required min-len:3"`&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"ＡＬＩＣＥ"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&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="c"&gt;// a.Name is now "ALICE"&lt;/span&gt;

&lt;span class="n"&gt;waf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;nfkc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ｒｍ　－ｒｆ　／"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;// waf is "rm -rf /"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are one word in a &lt;code&gt;checkers&lt;/code&gt; tag. Neither needs a second pass over your validation logic — they sit in the same pipeline as &lt;code&gt;trim&lt;/code&gt;, &lt;code&gt;lower&lt;/code&gt;, and &lt;code&gt;required&lt;/code&gt;, because normalizers and checkers are the same function type in this library and can be freely mixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; fix
&lt;/h2&gt;

&lt;p&gt;Be precise about what NFKC actually covers, because it's narrower than "Unicode spoofing" in general. NFKC only folds characters that are &lt;em&gt;compatibility-equivalent&lt;/em&gt; under the Unicode standard — the same letter, rendered differently. It does nothing for &lt;strong&gt;homoglyphs&lt;/strong&gt;: characters that are visually similar but come from a different script entirely, like a Cyrillic &lt;code&gt;а&lt;/code&gt; (U+0430) standing in for a Latin &lt;code&gt;a&lt;/code&gt; (U+0061). Those two are not NFKC-equivalent, so &lt;code&gt;paypal.com&lt;/code&gt; spelled with a Cyrillic &lt;code&gt;а&lt;/code&gt; normalizes right back to itself — still spoofed.&lt;/p&gt;

&lt;p&gt;Catching that class of lookalike needs a &lt;em&gt;confusables&lt;/em&gt; check — &lt;a href="https://unicode.org/reports/tr39/" rel="noopener noreferrer"&gt;Unicode UTS #39&lt;/a&gt;'s skeleton algorithm, which maps visually similar characters across scripts to a shared representative form before comparing. It's a genuinely different, heavier piece of machinery than either normalizer here, and &lt;code&gt;checker&lt;/code&gt; doesn't currently provide it. If cross-script impersonation is part of your threat model — a login handle, a domain-like identifier — &lt;code&gt;strip-invisible&lt;/code&gt; plus &lt;code&gt;nfkc&lt;/code&gt; narrows the problem but doesn't close it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to apply this, and where not to
&lt;/h2&gt;

&lt;p&gt;Both normalizers are opinionated about &lt;em&gt;removing or rewriting&lt;/em&gt; characters, which means they're right for the fields where an invisible or compatibility character is never legitimate — a username, a handle, a search keyword, an API key — and wrong for general free-text content. Zero-width joiner is load-bearing in emoji sequences; zero-width non-joiner has real uses in Persian and other scripts. Running &lt;code&gt;strip-invisible&lt;/code&gt; over a chat message body or a comment field will quietly corrupt text some of your users actually rely on. Point these at identifiers, not prose.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Registration&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Handle&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim strip-invisible nfkc required min-len:3 alphanumeric"`&lt;/span&gt;
    &lt;span class="n"&gt;Bio&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim max-len:500"`&lt;/span&gt; &lt;span class="c"&gt;// free text -- neither normalizer applies here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strip-invisible&lt;/code&gt; ships in &lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;Checker&lt;/a&gt;'s core module; &lt;code&gt;nfkc&lt;/code&gt; is a separate, opt-in module at &lt;a href="https://github.com/cinar/checker/tree/main/nfkc" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/cinar/checker/v2/nfkc&lt;/code&gt;&lt;/a&gt;, so the dependency it needs never touches a codebase that doesn't ask for it.&lt;/p&gt;

</description>
      <category>go</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Catch Bad Validation Tags at Compile Time with checkerlint</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:40:00 +0000</pubDate>
      <link>https://dev.to/onurcinar/catch-bad-validation-tags-at-compile-time-with-checkerlint-2iaa</link>
      <guid>https://dev.to/onurcinar/catch-bad-validation-tags-at-compile-time-with-checkerlint-2iaa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Struct tags are just strings — a typo'd checker name, a wrong-typed field, or a renamed cross-field target all compile fine and fail silently at runtime. checkerlint catches all three before you ship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Struct tags are string literals. The Go compiler checks that your struct compiles — it has no idea what &lt;code&gt;checkers:"eq-field:Passwrd"&lt;/code&gt; means, so a typo in a field name, a checker applied to a field of the wrong type, or a renamed field that a cross-field rule still points at all compile fine. They fail later, at runtime, sometimes silently, sometimes as a panic in the middle of handling a request.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Registration&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim required"`&lt;/span&gt;
    &lt;span class="n"&gt;ConfirmPassword&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required eq-field:Passwrd"`&lt;/span&gt; &lt;span class="c"&gt;// typo: no such field&lt;/span&gt;
    &lt;span class="n"&gt;Age&lt;/span&gt;             &lt;span class="kt"&gt;int&lt;/span&gt;    &lt;span class="s"&gt;`checkers:"email"`&lt;/span&gt;                     &lt;span class="c"&gt;// email is string-only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing here trips &lt;code&gt;go build&lt;/code&gt;, &lt;code&gt;go vet&lt;/code&gt;, or a normal linter — they all treat &lt;code&gt;checkers:"..."&lt;/code&gt; as an opaque string. The first bug only surfaces the moment someone submits a registration form and &lt;code&gt;eq-field&lt;/code&gt; can't find a field called &lt;code&gt;Passwrd&lt;/code&gt;. The second is worse: &lt;code&gt;email&lt;/code&gt; assumes a string under the hood, so calling it on an &lt;code&gt;int&lt;/code&gt; field panics at validation time instead of returning a normal error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cinar/checker/tree/main/checkerlint" rel="noopener noreferrer"&gt;&lt;code&gt;checkerlint&lt;/code&gt;&lt;/a&gt; is a &lt;code&gt;go/analysis&lt;/code&gt;-based static analyzer, shipped as its own module in the &lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;Checker&lt;/a&gt; repo, that reads these tags at build/lint time and catches exactly this class of bug before it ships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./registration.go:3:2: checkerlint: eq-field references field "Passwrd", which doesn't exist on this struct
./registration.go:4:2: checkerlint: email requires a string, but the field's type is int
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it actually checks
&lt;/h2&gt;

&lt;p&gt;Three things, all specific to how &lt;code&gt;checkers&lt;/code&gt;/&lt;code&gt;validate&lt;/code&gt; tags can go wrong:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unknown checker names.&lt;/strong&gt; Every token in the tag has to be a registered checker, normalizer, field-relative checker, &lt;code&gt;omitempty&lt;/code&gt;, or a name your own code registered via &lt;code&gt;RegisterMaker&lt;/code&gt;/&lt;code&gt;RegisterFieldMaker&lt;/code&gt; with a string literal. Typo &lt;code&gt;requird&lt;/code&gt; instead of &lt;code&gt;required&lt;/code&gt; and &lt;code&gt;checkerlint&lt;/code&gt; flags it — nothing else in your toolchain will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type compatibility.&lt;/strong&gt; A well-scoped set of built-ins panic at runtime on the wrong kind — string-only checkers (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;trim&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, and friends) and numeric-only ones (&lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;gte&lt;/code&gt;, &lt;code&gt;lt&lt;/code&gt;, &lt;code&gt;lte&lt;/code&gt;). &lt;code&gt;checkerlint&lt;/code&gt; checks the tagged field's actual type against what the checker expects, including through one level of pointer indirection and through a slice/array/map's element type for item-level tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-field targets.&lt;/strong&gt; &lt;code&gt;eq-field&lt;/code&gt;, &lt;code&gt;after-field&lt;/code&gt;, &lt;code&gt;before-field&lt;/code&gt;, &lt;code&gt;required-if&lt;/code&gt;, and &lt;code&gt;required-unless&lt;/code&gt; all name a sibling field by string. &lt;code&gt;checkerlint&lt;/code&gt; confirms that field actually exists on the struct — including, on a best-effort basis, embedded/anonymous fields — so a rename doesn't silently leave a dangling reference.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It knows the built-in vocabulary from whichever version of &lt;code&gt;github.com/cinar/checker/v2&lt;/code&gt; your code is built against, so it stays in sync automatically as new checkers ship. It can't see a checker registered only at runtime in a separate package, or one whose name is built from a non-literal expression — both are edge cases outside what static analysis can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Install the standalone binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/cinar/checker/v2/checkerlint/cmd/checkerlint@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it like &lt;code&gt;go vet&lt;/code&gt; — same flags, same package patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;checkerlint ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or plug it into &lt;code&gt;go vet&lt;/code&gt; directly as a vet tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go vet &lt;span class="nt"&gt;-vettool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;which checkerlint&lt;span class="si"&gt;)&lt;/span&gt; ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wiring it into golangci-lint
&lt;/h2&gt;

&lt;p&gt;If your CI already runs &lt;code&gt;golangci-lint&lt;/code&gt;, add &lt;code&gt;checkerlint&lt;/code&gt; as a module plugin (&lt;code&gt;golangci-lint&lt;/code&gt; v2's plugin system) instead of a separate step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .golangci.yml&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;span class="na"&gt;linters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;custom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;checkerlint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;module&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.com/cinar/checker/v2/checkerlint&lt;/span&gt;
        &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Module-plugin build steps have shifted across &lt;code&gt;golangci-lint&lt;/code&gt; releases, so check &lt;a href="https://golangci-lint.run/plugins/module-plugins/" rel="noopener noreferrer"&gt;its module-plugin docs&lt;/a&gt; for the exact setup your version expects — the &lt;code&gt;.golangci.yml&lt;/code&gt; shape above is the stable part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than it sounds like
&lt;/h2&gt;

&lt;p&gt;Every struct-tag-driven validation library has this exact blind spot — &lt;code&gt;go-playground/validator&lt;/code&gt;, &lt;code&gt;ozzo-validation&lt;/code&gt;, all of them. The tags are strings; the compiler can't see inside them. Most teams find out about a typo'd checker name or a stale cross-field reference the way you'd expect: a bug report, or a support ticket about a validation rule that "just doesn't work," usually for a code path that isn't covered by a test with that exact malformed input.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;checkerlint&lt;/code&gt; moves that failure from "someone hits it in production" to "the linter fails your PR." That's a category of bug — silently wrong or panicking struct tags — that the rest of the Go toolchain has no way to see, closed at the point where it's cheapest to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/cinar/checker/v2/checkerlint/cmd/checkerlint@latest
checkerlint ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full details, including exactly which checkers are type-checked, are in the &lt;a href="https://github.com/cinar/checker/tree/main/checkerlint" rel="noopener noreferrer"&gt;checkerlint README&lt;/a&gt;. It's a separate, independently versioned module — adding it to your CI doesn't add a single dependency to the core &lt;code&gt;checker&lt;/code&gt; library your code actually imports.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Returning RFC 9457 Problem Details from Go Validation Errors</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:32:51 +0000</pubDate>
      <link>https://dev.to/onurcinar/returning-rfc-9457-problem-details-from-go-validation-errors-4nb7</link>
      <guid>https://dev.to/onurcinar/returning-rfc-9457-problem-details-from-go-validation-errors-4nb7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How to turn struct-tag validation failures into a standard "application/problem+json" response — the RFC 9457 format — with one method call, no hand-rolled error envelope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most Go APIs invent their own validation error shape. One team returns &lt;code&gt;{"errors": [...]}&lt;/code&gt;, another &lt;code&gt;{"field_errors": {...}}&lt;/code&gt;, a third just a flat &lt;code&gt;{"error": "message"}&lt;/code&gt; and hopes the client parses it. Every one of those is a private contract the client has to learn from your docs, because there's no shared shape for "here's what's wrong with your request."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9457" rel="noopener noreferrer"&gt;RFC 9457&lt;/a&gt; — &lt;em&gt;Problem Details for HTTP APIs&lt;/em&gt; — is the IETF standard that fixes this: a &lt;code&gt;application/problem+json&lt;/code&gt; body with &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and room for problem-specific extensions. &lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;Checker&lt;/a&gt; now builds one of these directly from a failed struct validation, via &lt;code&gt;CheckErrors.ProblemDetails()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;RFC 9457 defines four base members — &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;detail&lt;/code&gt;, &lt;code&gt;instance&lt;/code&gt; — and lets a specific problem type add its own. For validation errors, &lt;a href="https://www.rfc-editor.org/rfc/rfc9457#section-3.1" rel="noopener noreferrer"&gt;RFC 9457 §3.1&lt;/a&gt; sketches exactly this extension: an &lt;code&gt;invalid-params&lt;/code&gt; array listing which fields failed and why. That's what Checker produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a failed struct to a problem+json body
&lt;/h2&gt;

&lt;p&gt;Take a struct with a missing required field:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;errs&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;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person&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;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&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;data&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"about:blank"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your request parameters failed validation."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invalid-params"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Required value is missing."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQUIRED"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One method call — &lt;code&gt;errs.ProblemDetails()&lt;/code&gt; — turns the same &lt;code&gt;CheckErrors&lt;/code&gt; you'd otherwise call &lt;code&gt;.JSON()&lt;/code&gt; on into a &lt;code&gt;*ProblemDetails&lt;/code&gt; value, ready to marshal. &lt;code&gt;type&lt;/code&gt; defaults to &lt;code&gt;"about:blank"&lt;/code&gt; (RFC 9457's own default for "no more specific problem type registered"), &lt;code&gt;status&lt;/code&gt; defaults to &lt;code&gt;400&lt;/code&gt;, and each &lt;code&gt;invalid-params&lt;/code&gt; entry carries the field &lt;code&gt;name&lt;/code&gt;, a localized human-readable &lt;code&gt;reason&lt;/code&gt;, and the machine-readable &lt;code&gt;code&lt;/code&gt; — the same code you'd branch on client-side with the plain &lt;code&gt;.JSON()&lt;/code&gt; output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into a handler
&lt;/h2&gt;

&lt;p&gt;The one thing RFC 9457 requires that a plain JSON body doesn't is the content type:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;signupHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;SignupRequest&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid JSON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&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;valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pd&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/problem+json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&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;&lt;code&gt;pd.Status&lt;/code&gt; drives both the header and the body, so they can't drift out of sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overriding the defaults
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Type&lt;/code&gt;, &lt;code&gt;Title&lt;/code&gt;, and &lt;code&gt;Status&lt;/code&gt; are plain exported fields on the returned &lt;code&gt;*ProblemDetails&lt;/code&gt; — RFC 9457 deliberately leaves them to the API producer, so Checker fills in reasonable defaults and gets out of your way. Set them before marshaling if you want a registered problem type or a different status code:&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;pd&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProblemDetails&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://api.example.com/problems/validation-error"&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Signup request failed validation"&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusUnprocessableEntity&lt;/span&gt; &lt;span class="c"&gt;// 422 instead of 400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else about the call changes — &lt;code&gt;invalid-params&lt;/code&gt; is still built from the same &lt;code&gt;CheckErrors&lt;/code&gt; map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localized problem details
&lt;/h2&gt;

&lt;p&gt;Since &lt;code&gt;ProblemDetails()&lt;/code&gt; is just &lt;code&gt;ProblemDetailsWithLocale(DefaultLocale)&lt;/code&gt; under the hood, a localized variant is one argument away — same &lt;a href="https://github.com/cinar/checker#localized-error-messages" rel="noopener noreferrer"&gt;23 locales&lt;/a&gt; that back &lt;code&gt;JSONWithLocale&lt;/code&gt;:&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;pd&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProblemDetailsWithLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeDE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;reason&lt;/code&gt; strings come back in German; &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;code&lt;/code&gt; are untouched, since those aren't meant for display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with a standard shape
&lt;/h2&gt;

&lt;p&gt;A hand-rolled error envelope works fine until you have more than one API, or a client library trying to handle errors generically, or an API gateway that wants to do something useful with 4xx bodies without special-casing your service. &lt;code&gt;application/problem+json&lt;/code&gt; is already understood by tooling in several ecosystems — Spring, ASP.NET Core, and various API gateways emit or consume it natively — so producing it from Go costs you nothing extra (it's the same validation you were already running) and buys you a body shape other tools don't have to be taught.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/cinar/checker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ProblemDetails()&lt;/code&gt; and &lt;code&gt;ProblemDetailsWithLocale()&lt;/code&gt; sit right next to &lt;code&gt;JSON()&lt;/code&gt; and &lt;code&gt;JSONWithLocale()&lt;/code&gt; on &lt;code&gt;CheckErrors&lt;/code&gt; — pick whichever body shape your API needs, from the same validation call. Full details in the &lt;a href="https://github.com/cinar/checker#structured-errors" rel="noopener noreferrer"&gt;README&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>go</category>
      <category>api</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Struct Tag Validation in Go: A Practical Guide</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 06 Sep 2026 03:26:27 +0000</pubDate>
      <link>https://dev.to/onurcinar/struct-tag-validation-in-go-a-practical-guide-j4d</link>
      <guid>https://dev.to/onurcinar/struct-tag-validation-in-go-a-practical-guide-j4d</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A hands-on walkthrough of validating and normalizing a real signup form in Go — trimming input, cross-field rules, slices, custom messages, and JSON error responses — using struct tags.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every Go API ends up needing the same thing: take a JSON body, clean it up, make sure it's valid, and tell the client exactly what's wrong if it isn't. This guide walks through building that for a real signup form, using struct tags instead of hand-written &lt;code&gt;if&lt;/code&gt; chains, with &lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;Checker&lt;/a&gt; as the tool doing the work.&lt;/p&gt;

&lt;p&gt;By the end you'll have a &lt;code&gt;SignupRequest&lt;/code&gt; that trims and lowercases its own input, validates it, enforces a cross-field password match, checks a slice of roles, returns friendly per-field error messages, and serializes cleanly to JSON — in about 15 lines of tags.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/cinar/checker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: A struct with checkers, not &lt;code&gt;if&lt;/code&gt; statements
&lt;/h2&gt;

&lt;p&gt;Start with a plain struct and describe each field's rules as a &lt;code&gt;checkers&lt;/code&gt; tag:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SignupRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;           &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"email" checkers:"trim lower required email"`&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"password" checkers:"required min-len:8"`&lt;/span&gt;
    &lt;span class="n"&gt;ConfirmPassword&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"confirm_password" checkers:"required eq-field:Password"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read a tag left to right — it's a pipeline, executed in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;trim lower required email&lt;/code&gt; on &lt;code&gt;Email&lt;/code&gt;: trim whitespace, lowercase it, make sure something is left, then check it looks like an email. Order matters here — &lt;code&gt;required&lt;/code&gt; runs &lt;em&gt;after&lt;/em&gt; &lt;code&gt;trim&lt;/code&gt;, so a field of just &lt;code&gt;"   "&lt;/code&gt; correctly fails.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;required min-len:8&lt;/code&gt; on &lt;code&gt;Password&lt;/code&gt;: must be present, must be at least 8 characters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;required eq-field:Password&lt;/code&gt; on &lt;code&gt;ConfirmPassword&lt;/code&gt;: must be present, and must equal the sibling &lt;code&gt;Password&lt;/code&gt; field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that &lt;code&gt;eq-field&lt;/code&gt; isn't comparing against a hardcoded value — it's reading another field off the same struct at validation time. That's a "field-relative" checker; Checker also ships &lt;code&gt;after-field&lt;/code&gt;, &lt;code&gt;before-field&lt;/code&gt;, &lt;code&gt;required-if&lt;/code&gt;, and &lt;code&gt;required-unless&lt;/code&gt; for the same pattern (a state field required only when country is &lt;code&gt;"US"&lt;/code&gt;, a return date that has to be after a departure date, and so on).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Validate it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;SignupRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;"  ALICE@EXAMPLE.COM  "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;"supersecret123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ConfirmPassword&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"supersecret123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&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;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// "alice@example.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CheckStruct&lt;/code&gt; does two things in one pass: it normalizes the struct &lt;strong&gt;in-place&lt;/strong&gt; and validates it. Look at &lt;code&gt;req.Email&lt;/code&gt; after the call — it's already trimmed and lowercased, ready to hash the password against or write to a database, with no separate "sanitize" step beforehand.&lt;/p&gt;

&lt;p&gt;If validation fails, &lt;code&gt;errs&lt;/code&gt; is a &lt;code&gt;CheckErrors&lt;/code&gt; — a &lt;code&gt;map[string]error&lt;/code&gt; keyed by field name, which also implements &lt;code&gt;error&lt;/code&gt; itself, so you can &lt;code&gt;return errs&lt;/code&gt; directly from a function that expects one. Calling &lt;code&gt;.JSON()&lt;/code&gt; on it gives you an HTTP-API-ready body:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Password"&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="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_MIN_LEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Value cannot be less than 8."&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="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;Machine-readable &lt;code&gt;code&lt;/code&gt; for client-side logic, human-readable &lt;code&gt;message&lt;/code&gt; for display — per field, no manual formatting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add a slice field
&lt;/h2&gt;

&lt;p&gt;Signup forms often collect more than scalars. Say &lt;code&gt;SignupRequest&lt;/code&gt; also takes a list of roles, capped at 3, each one trimmed and alphanumeric-only:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SignupRequest&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;           &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"email" checkers:"trim lower required email"`&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"password" checkers:"required min-len:8"`&lt;/span&gt;
    &lt;span class="n"&gt;ConfirmPassword&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;   &lt;span class="s"&gt;`json:"confirm_password" checkers:"required eq-field:Password"`&lt;/span&gt;
    &lt;span class="n"&gt;Roles&lt;/span&gt;           &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"roles" checkers:"@max-len:3 trim alphanumeric"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@&lt;/code&gt; prefix is what separates &lt;em&gt;container&lt;/em&gt;-level rules from &lt;em&gt;item&lt;/em&gt;-level ones in the same tag. &lt;code&gt;@max-len:3&lt;/code&gt; caps the slice at three entries; &lt;code&gt;trim alphanumeric&lt;/code&gt; (no &lt;code&gt;@&lt;/code&gt;) runs on every individual string inside it. Given &lt;code&gt;Roles: []string{"  admin  ", "editor!"}&lt;/code&gt;, after &lt;code&gt;CheckStruct&lt;/code&gt; runs, &lt;code&gt;Roles[0]&lt;/code&gt; becomes &lt;code&gt;"admin"&lt;/code&gt; and &lt;code&gt;Roles[1]&lt;/code&gt; fails &lt;code&gt;alphanumeric&lt;/code&gt; because of the &lt;code&gt;!&lt;/code&gt;. The same &lt;code&gt;@&lt;/code&gt; split works for maps, and nested structs inside slices/maps are walked recursively — this isn't a top-level-fields-only validator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Make a field optional
&lt;/h2&gt;

&lt;p&gt;Not every field should be required. Add an optional website field:&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;Website&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"website" checkers:"omitempty url"`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;omitempty&lt;/code&gt; skips every other checker in the tag when the field is its zero value, but still runs them normally once a value is present. An empty &lt;code&gt;Website&lt;/code&gt; is fine; &lt;code&gt;"not-a-url"&lt;/code&gt; is not. It looks at the field's &lt;em&gt;original&lt;/em&gt; value, so &lt;code&gt;trim omitempty required&lt;/code&gt; on an all-whitespace string still fails &lt;code&gt;required&lt;/code&gt; after trimming — whitespace isn't the zero value to begin with. (Don't pair &lt;code&gt;omitempty&lt;/code&gt; with &lt;code&gt;required&lt;/code&gt; on the same field — that's a contradiction, and &lt;code&gt;omitempty&lt;/code&gt; wins, so &lt;code&gt;required&lt;/code&gt; never runs.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Give one field a friendlier error message
&lt;/h2&gt;

&lt;p&gt;The default messages are fine for logs, but a signup form probably wants nicer copy for &lt;code&gt;Email&lt;/code&gt; specifically. Add a &lt;code&gt;checkersMsg&lt;/code&gt; tag alongside &lt;code&gt;checkers&lt;/code&gt; — a semicolon-separated list of &lt;code&gt;name=message&lt;/code&gt; pairs, keyed by the bare checker name:&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;Email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"email" checkers:"trim lower required email" checkersMsg:"required=Email is required;email=Enter a valid email address"`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This overrides the message only for this field, only for these checkers — every other field using &lt;code&gt;required&lt;/code&gt; or &lt;code&gt;email&lt;/code&gt; elsewhere in your codebase keeps the default (or localized) wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Wire it into an HTTP handler
&lt;/h2&gt;

&lt;p&gt;Put it all together with the standard library — no framework required:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;signupHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;SignupRequest&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid JSON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&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;valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// req is normalized and valid — safe to persist.&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&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;If you're on Gin or Echo instead, the separately-versioned adapter modules collapse decode-and-validate into one call:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checkergin&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2/gin"&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/signup"&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;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="n"&gt;SignupRequest&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;checkergin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&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="c"&gt;// 400 already written&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&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;h2&gt;
  
  
  What you get for free once the tags exist
&lt;/h2&gt;

&lt;p&gt;Because the rules live as tags on the type rather than scattered across handler code, they're reusable outside validation itself. &lt;code&gt;checker.JSONSchema(&amp;amp;SignupRequest{})&lt;/code&gt; walks the same tags and produces a Draft 2020-12 JSON Schema document — &lt;code&gt;required&lt;/code&gt; becomes &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;min-len&lt;/code&gt; becomes &lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt; becomes &lt;code&gt;format: "email"&lt;/code&gt; — so your Go validation rules can double as API documentation or a frontend contract, instead of being hand-copied into a second place that quietly drifts out of sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/cinar/checker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a runnable version of the core trim/validate/cross-field pattern on the &lt;a href="https://go.dev/play/p/FfkXm5oC9ii" rel="noopener noreferrer"&gt;Go Playground&lt;/a&gt;, and the full checker list — 30+ built-ins covering emails, URLs, IPs, credit cards, hashes, country codes, and more — is in the &lt;a href="https://github.com/cinar/checker#checkers-provided" rel="noopener noreferrer"&gt;README&lt;/a&gt;. If a rule you need isn't built in, &lt;code&gt;RegisterMaker&lt;/code&gt; lets you add your own and it behaves exactly like a first-party checker in struct tags.&lt;/p&gt;

</description>
      <category>go</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stop Hand-Rolling Validation in Go — Meet Checker</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sat, 05 Sep 2026 04:23:29 +0000</pubDate>
      <link>https://dev.to/onurcinar/stop-hand-rolling-validation-in-go-meet-checker-nk1</link>
      <guid>https://dev.to/onurcinar/stop-hand-rolling-validation-in-go-meet-checker-nk1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzehiurnma17kqip9tqp7.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzehiurnma17kqip9tqp7.gif" alt="GIF showing Checker running" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've written more than one HTTP handler in Go, you've written this code:&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name is required"&lt;/span&gt;&lt;span class="p"&gt;)&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;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"@"&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="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfirmPassword&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"passwords do not match"&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;It starts small. Then a new field shows up, then a cross-field rule, then someone asks for a nicer error message, then someone else asks for that error message in Spanish, and suddenly your handler is 200 lines of &lt;code&gt;if&lt;/code&gt; statements that nobody wants to touch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;&lt;strong&gt;Checker&lt;/strong&gt;&lt;/a&gt; is a Go library built to make that pile of &lt;code&gt;if&lt;/code&gt; statements disappear — declaratively, with zero external dependencies. Here's why it's worth a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line pitch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Registration&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim required"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt;           &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required email"`&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt;        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required min-len:8"`&lt;/span&gt;
    &lt;span class="n"&gt;ConfirmPassword&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"eq-field:Password"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;registration&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;valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// errors is a map[string]error, keyed by field name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Trim the name, require it, validate the email format, enforce a minimum password length, and confirm the two password fields match — all declared next to the field they apply to, all in one call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's worth your attention
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Zero dependencies, really zero
&lt;/h3&gt;

&lt;p&gt;The core &lt;code&gt;checker&lt;/code&gt; module imports nothing beyond the Go standard library. Not &lt;code&gt;reflect&lt;/code&gt;-based validator forks with a dozen transitive deps, not a YAML parser you didn't ask for. &lt;code&gt;go get github.com/cinar/checker/v2&lt;/code&gt; pulls in exactly one thing: Checker itself. That matters for supply-chain surface area, build times, and not having to explain to a security review why your validation library needs 40 packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checkers &lt;em&gt;and&lt;/em&gt; normalizers, in one pipeline
&lt;/h3&gt;

&lt;p&gt;Most validation libraries only check — they tell you your input is wrong. Checker also fixes it. &lt;code&gt;trim&lt;/code&gt;, &lt;code&gt;lower&lt;/code&gt;, &lt;code&gt;upper&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, HTML/URL escaping — these are normalizers, and they share the exact same pipeline as checkers like &lt;code&gt;required&lt;/code&gt; or &lt;code&gt;email&lt;/code&gt;. Mix them freely:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"trim title required"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trim the whitespace, title-case it, then make sure something is actually left. One declaration, no separate "sanitize" pass before your "validate" pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-field and conditional rules, without a callback
&lt;/h3&gt;

&lt;p&gt;A password-confirmation field. A "State" field that's only required if "Country" is "US". A return date that has to be after the departure date. These usually mean dropping out of struct tags entirely and writing custom validation functions. Checker handles them as tags:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Trip&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Country&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required"`&lt;/span&gt;
    &lt;span class="n"&gt;State&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required-if:Country:US"`&lt;/span&gt;
    &lt;span class="n"&gt;DepartAt&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required"`&lt;/span&gt;
    &lt;span class="n"&gt;ReturnAt&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"required after-field:DateOnly:DepartAt"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;eq-field&lt;/code&gt;, &lt;code&gt;required-if&lt;/code&gt;, &lt;code&gt;required-unless&lt;/code&gt;, &lt;code&gt;before-field&lt;/code&gt;, &lt;code&gt;after-field&lt;/code&gt; — all comparing sibling struct fields, all declared inline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Slices and maps, checked at both levels
&lt;/h3&gt;

&lt;p&gt;Need to make sure a slice has at most 2 emails, &lt;em&gt;and&lt;/em&gt; that each email is at most 64 characters? The &lt;code&gt;@&lt;/code&gt; prefix separates container-level rules from item-level ones:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Emails&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="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`checkers:"@max-len:2 trim max-len:64"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@max-len:2&lt;/code&gt; caps the map at two entries. &lt;code&gt;trim max-len:64&lt;/code&gt; runs on every value in it. Nested structs and pointers inside a slice or map get walked and checked too — this isn't a shallow, top-level-fields-only validator.&lt;/p&gt;

&lt;h3&gt;
  
  
  23 languages, opt-in
&lt;/h3&gt;

&lt;p&gt;Ship a SaaS product to a global audience and "Not a valid email address." in every locale stops being acceptable pretty fast. Checker ships &lt;strong&gt;23 translated locales&lt;/strong&gt; out of the box — the same set &lt;code&gt;go-playground/validator&lt;/code&gt; supports — and none of them cost you anything unless you ask for them:&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;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeDE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeDEMessages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"abcd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrorWithLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeDE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c"&gt;// Keine gültige E-Mail-Adresse.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;en-US&lt;/code&gt; is registered by default, so importing &lt;code&gt;checker&lt;/code&gt; never silently pulls translation data into your binary that you don't use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured errors that are API-ready
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CheckStruct&lt;/code&gt; doesn't just hand you a generic &lt;code&gt;error&lt;/code&gt;. It returns &lt;code&gt;CheckErrors&lt;/code&gt;, a &lt;code&gt;map[string]error&lt;/code&gt; keyed by field name that &lt;em&gt;also&lt;/em&gt; implements the &lt;code&gt;error&lt;/code&gt; interface, so you can return it directly. When you're building an HTTP API, call &lt;code&gt;.JSON()&lt;/code&gt; and you're done:&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;errs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckStruct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;registration&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;valid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// {"Name":{"code":"REQUIRED","message":"Required value is missing."}}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Machine-readable &lt;code&gt;code&lt;/code&gt;, human-readable &lt;code&gt;message&lt;/code&gt;, per field, ready to serialize. &lt;code&gt;JSONWithLocale()&lt;/code&gt; does the same thing localized.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your validation rules, turned into a JSON Schema — for free
&lt;/h3&gt;

&lt;p&gt;This is the feature that stops people mid-scroll: Checker can generate a &lt;strong&gt;JSON Schema&lt;/strong&gt; document directly from your struct tags.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Person&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"name" checkers:"trim required"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"email" checkers:"required email"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONSchema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Person"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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="nl"&gt;"email"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&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="nl"&gt;"name"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"name"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;required&lt;/code&gt; becomes &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;min-len&lt;/code&gt;/&lt;code&gt;max-len&lt;/code&gt; become &lt;code&gt;minLength&lt;/code&gt;/&lt;code&gt;maxLength&lt;/code&gt; (or &lt;code&gt;minItems&lt;/code&gt;/&lt;code&gt;maxItems&lt;/code&gt;, &lt;code&gt;minProperties&lt;/code&gt;/&lt;code&gt;maxProperties&lt;/code&gt; for slices and maps), &lt;code&gt;gte&lt;/code&gt;/&lt;code&gt;lte&lt;/code&gt; become &lt;code&gt;minimum&lt;/code&gt;/&lt;code&gt;maximum&lt;/code&gt;, and &lt;code&gt;email&lt;/code&gt;/&lt;code&gt;url&lt;/code&gt;/&lt;code&gt;ipv4&lt;/code&gt;/&lt;code&gt;fqdn&lt;/code&gt; become a &lt;code&gt;format&lt;/code&gt;. A checker with no schema equivalent isn't silently dropped — it lands in an &lt;code&gt;x-checker&lt;/code&gt; vendor extension so nothing goes missing.&lt;/p&gt;

&lt;p&gt;Translation: your Go validation tags &lt;em&gt;are&lt;/em&gt; your API documentation and your frontend validation spec. Stop maintaining the same rules three times.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drop-in adapters for Gin and Echo
&lt;/h3&gt;

&lt;p&gt;If you're on Gin or Echo, binding and validating a request body is one call:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checkergin&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2/gin"&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/register"&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;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;gin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;registration&lt;/span&gt; &lt;span class="n"&gt;Registration&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;checkergin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;registration&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="c"&gt;// 400 already written&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;registration&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;Both adapters are separately-versioned Go modules, so Gin or Echo only enters your dependency tree if you actually &lt;code&gt;go get&lt;/code&gt; the adapter — the core library stays dependency-free regardless of which framework you use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensible when the built-ins aren't enough
&lt;/h3&gt;

&lt;p&gt;Checker ships 30+ checkers (email, URL, IP/IPv4/IPv6, CIDR, MAC, credit card, ISBN, hashes, country and language codes, an Ethereum address checker, and more), but you're not boxed in. Register your own:&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;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterMaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"is-fruit"&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;params&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;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CheckFunc&lt;/span&gt;&lt;span class="p"&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;Value&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="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Interface&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;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"apple"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"banana"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCheckError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NOT_FRUIT"&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;Once registered, &lt;code&gt;is-fruit&lt;/code&gt; works in struct tags exactly like a built-in checker — and you can teach &lt;code&gt;JSONSchema&lt;/code&gt; how to represent it too, via &lt;code&gt;RegisterSchemaMaker&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built like a library that means it
&lt;/h2&gt;

&lt;p&gt;A detail that's easy to gloss over: this project enforces &lt;strong&gt;100% test coverage&lt;/strong&gt;. Every checker, every normalizer, every branch has a matching test. There's also a &lt;code&gt;locales_test.go&lt;/code&gt; that fails the build if any locale is missing a message for a given error code, or if a placeholder doesn't match &lt;code&gt;en-US&lt;/code&gt;. That's the kind of quiet discipline that keeps a validation library from lying to you in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go get github.com/cinar/checker/v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;checker&lt;/span&gt; &lt;span class="s"&gt;"github.com/cinar/checker/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the project on GitHub: &lt;strong&gt;&lt;a href="https://github.com/cinar/checker" rel="noopener noreferrer"&gt;github.com/cinar/checker&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're validating structs by hand in Go right now, give it five minutes. If you find a checker missing, or a locale that's rough around the edges — PRs are welcome.&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Self-Evolving Apps: Not Vibe, Live-Coding in Ruby with LLMs and Metaprogramming</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Wed, 12 Aug 2026 05:28:04 +0000</pubDate>
      <link>https://dev.to/onurcinar/self-evolving-apps-not-vibe-live-coding-in-ruby-with-llms-and-metaprogramming-56h1</link>
      <guid>https://dev.to/onurcinar/self-evolving-apps-not-vibe-live-coding-in-ruby-with-llms-and-metaprogramming-56h1</guid>
      <description>&lt;p&gt;Using AI to generate code for a new application is a familiar workflow today. But what if an application starts as a completely blank slate, learning on the job and writing its own implementation live as you call nonexistent methods?&lt;/p&gt;

&lt;p&gt;This concept of live-patching and zero-downtime execution isn't entirely new. Early in my career at &lt;strong&gt;Nortel Networks&lt;/strong&gt;, I have seen this with &lt;strong&gt;PROTEL&lt;/strong&gt; (PRocess Oriented TELepony language), a proprietary language designed for telecom switches. To achieve "five nines" (99.999%) availability, you couldn't simply take systems offline for deployments—code updates had to happen via live hot-patching. Later on, I thoroughly enjoyed similar live code reloading capabilities while working with &lt;strong&gt;Erlang&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Recently, as I spent more time with &lt;strong&gt;Ruby&lt;/strong&gt;, its rich metaprogramming capabilities got me thinking: &lt;em&gt;What if we start with an empty Ruby object, and as we call methods on it, it uses an LLM to write and evaluate its own code on the fly?&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Blank Slate
&lt;/h2&gt;

&lt;p&gt;Let's start with a completely empty class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dummy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;LiveCode&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's fire up &lt;code&gt;irb&lt;/code&gt; (Ruby's REPL) and start interacting with our dummy object as if the methods already existed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;dummy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, Ruby will complain because &lt;code&gt;Dummy&lt;/code&gt; doesn't have an &lt;code&gt;add&lt;/code&gt; method. However, the method name (&lt;code&gt;add&lt;/code&gt;) and its arguments (&lt;code&gt;1, 2&lt;/code&gt;) clearly communicate our intent. &lt;/p&gt;

&lt;p&gt;Ruby provides a built-in hook called &lt;code&gt;method_missing&lt;/code&gt; to catch calls to undefined methods. This is where we bring in AI. To interact with our LLM provider, we'll use the excellent &lt;a href="https://rubyllm.com/" rel="noopener noreferrer"&gt;RubyLLM&lt;/a&gt; gem.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Dynamic Method Generation via &lt;code&gt;method_missing&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To make this behavior reusable across objects, we put our logic inside a &lt;code&gt;LiveCode&lt;/code&gt; base class that &lt;code&gt;Dummy&lt;/code&gt; inherits from.&lt;/p&gt;

&lt;p&gt;Here is our initial &lt;code&gt;LiveCode&lt;/code&gt; implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LiveCode&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;method_missing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Missing method: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;&lt;span class="sh"&gt;
      You are a Ruby code generator. A missing method `&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;` was called
      with the arguments: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;, keyword arguments: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;.

      Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
      Example:
      def &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;(...)
        # implementation
      end
&lt;/span&gt;&lt;span class="no"&gt;    PROMPT&lt;/span&gt;

    &lt;span class="n"&gt;ruby_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;

    &lt;span class="c1"&gt;# Evaluate the generated Ruby code directly on the instance's singleton class&lt;/span&gt;
    &lt;span class="n"&gt;singleton_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ruby_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Re-dispatch the original method call now that it exists!&lt;/span&gt;
    &lt;span class="nb"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interception&lt;/strong&gt;: When &lt;code&gt;dummy.add(1, 2)&lt;/code&gt; is called, &lt;code&gt;method_missing&lt;/code&gt; intercepts the call and extracts the method name (&lt;code&gt;:add&lt;/code&gt;) and parameter types (&lt;code&gt;Integer, Integer&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Prompting&lt;/strong&gt;: We construct a prompt instructing the model to return &lt;em&gt;only&lt;/em&gt; valid Ruby code defining the method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metaprogramming&lt;/strong&gt;: We use &lt;code&gt;singleton_class.class_eval(ruby_code)&lt;/code&gt; to inject the generated method into our object at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-dispatch&lt;/strong&gt;: Finally, &lt;code&gt;send(method_name, ...)&lt;/code&gt; invokes the newly defined method seamlessly!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Seeing It in Action
&lt;/h2&gt;

&lt;p&gt;Let's test this in IRB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight irb"&gt;&lt;code&gt;&lt;span class="gp"&gt;irb(main):003&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Missing method: add, [1, 2], {}"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Prompt: You are a Ruby code generator. A missing method `add` was called
with the arguments: [Integer, Integer], keyword arguments: [].
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def add(...)
&lt;/span&gt;&lt;span class="c"&gt;  # implementation
&lt;/span&gt;&lt;span class="go"&gt;end
"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Code: def add(a, b)
  a + b
end"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In real-time, the LLM synthesized &lt;code&gt;def add(a, b); a + b; end&lt;/code&gt;, registered it on &lt;code&gt;dummy&lt;/code&gt;, executed it, and returned &lt;code&gt;3&lt;/code&gt;. Subsequent calls to &lt;code&gt;dummy.add(1, 2)&lt;/code&gt; will execute instantly without hitting &lt;code&gt;method_missing&lt;/code&gt; again!&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Adding Context: State and Inter-Method Dependencies
&lt;/h2&gt;

&lt;p&gt;A real object has multiple methods that need to share state via instance variables and interact with one another. To enable this, our LLM needs context about existing instance variables, their types, and previously generated methods.&lt;/p&gt;

&lt;p&gt;We introduce a helper method &lt;code&gt;llm_context&lt;/code&gt; to capture this state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_context&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"# Current Instance Variables and Types:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;instance_variables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"(No instance variables yet)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nb"&gt;instance_variables&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;instance_variable_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;" - &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"# Previously Generated Methods:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@_generated_methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="vi"&gt;@_generated_methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"(No generated methods yet)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="vi"&gt;@_generated_methods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;context&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then update our prompt to include this rich context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;&lt;span class="sh"&gt;
  You are a Ruby code generator. A missing method `&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;` was called
  with the arguments: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;, keyword arguments: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;.

  Write ONLY the valid Ruby code to define `&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;`. Ensure it works
  well with the existing instance variables and previously generated methods.

  &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;llm_context&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;

  Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
  Example:
  def &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;method_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;(...)
    # implementation
  end
&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. Stateful Walkthrough: Setters &amp;amp; Getters
&lt;/h2&gt;

&lt;p&gt;Let's test setting a property and then retrieving it in a subsequent call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setting a Value
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight irb"&gt;&lt;code&gt;&lt;span class="gp"&gt;irb(main):003&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Onur"&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Missing method: name=, [\"Onur\"], {}"
"Prompt: You are a Ruby code generator. A missing method `name=` was called
with the arguments: [String], keyword arguments: [].
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Write ONLY the valid Ruby code to define `name=`. Ensure it works
well with the existing instance variables and previously generated methods.
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Current Instance Variables and Types:
&lt;/span&gt;&lt;span class="go"&gt; - @chat: RubyLLM::Chat
&lt;/span&gt;&lt;span class="c"&gt;# Previously Generated Methods:
&lt;/span&gt;&lt;span class="go"&gt;(No generated methods yet)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def name=(...)
&lt;/span&gt;&lt;span class="c"&gt;  # implementation
&lt;/span&gt;&lt;span class="go"&gt;end
"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Code: def name=(value)
  @name = value
end"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Onur"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Calling &lt;code&gt;dummy.name = "Onur"&lt;/code&gt; dynamically created the setter method &lt;code&gt;name=(value)&lt;/code&gt; which initialized the &lt;code&gt;@name&lt;/code&gt; instance variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Reading the Value
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight irb"&gt;&lt;code&gt;&lt;span class="gp"&gt;irb(main):004&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Missing method: name, [], {}"
"Prompt: You are a Ruby code generator. A missing method `name` was called
with the arguments: [], keyword arguments: [].
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Write ONLY the valid Ruby code to define `name`. Ensure it works
well with the existing instance variables and previously generated methods.
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Current Instance Variables and Types:
&lt;/span&gt;&lt;span class="go"&gt; - @chat: RubyLLM::Chat
 - @_generated_methods: Hash
 - @name: String
&lt;/span&gt;&lt;span class="c"&gt;# Previously Generated Methods:
&lt;/span&gt;&lt;span class="go"&gt;def name=(value)
  @name = value
end
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;Return ONLY valid Ruby code defining this method. Do not include markdown formatting.
Example:
def name(...)
&lt;/span&gt;&lt;span class="c"&gt;  # implementation
&lt;/span&gt;&lt;span class="go"&gt;end
"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="go"&gt;"Code: def name
  @name
end"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Onur"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because our prompt included &lt;code&gt;@name: String&lt;/code&gt; and the previously defined &lt;code&gt;name=(value)&lt;/code&gt; method, the LLM understood the context and generated the exact matching getter method &lt;code&gt;def name; @name; end&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;This experiment demonstrates how easily Ruby's dynamic nature combines with LLMs to build self-assembling objects. There are a few natural next steps for expanding this idea—such as persisting the generated code to disk, adding sandboxing/security checks, or enabling feedback loops to auto-fix runtime errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Code &amp;amp; Examples
&lt;/h2&gt;

&lt;p&gt;The complete working source code for this post is available in the site repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://zdo.com/blog/posts/self-evolving-app-code/" rel="noopener noreferrer"&gt;https://zdo.com/blog/posts/self-evolving-app-code/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>ai</category>
      <category>llm</category>
      <category>metaprogramming</category>
    </item>
    <item>
      <title>Stop Choosing One AI Coding Assistant: How I Pair Gemini CLI and OpenCode for Better Code</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sat, 02 May 2026 20:28:25 +0000</pubDate>
      <link>https://dev.to/onurcinar/stop-choosing-one-ai-coding-assistant-how-i-pair-gemini-cli-and-opencode-for-better-code-3op6</link>
      <guid>https://dev.to/onurcinar/stop-choosing-one-ai-coding-assistant-how-i-pair-gemini-cli-and-opencode-for-better-code-3op6</guid>
      <description>&lt;p&gt;If you’re like me, you’ve toggled between AI coding assistants trying to find the "best" one. Gemini generates features fast, while OpenCode’s models are good for catching edge cases. But why choose?&lt;/p&gt;

&lt;p&gt;I built a custom workflow using &lt;strong&gt;Gemini CLI&lt;/strong&gt; to orchestrate three specialized agents that bridge these two worlds. Here’s how I get the best of both: Gemini's speed for implementation and OpenCode's rigor for review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Agent Setup
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;.agents&lt;/code&gt; directory contains three distinct roles. The magic of Gemini CLI is its ability to not only write code but also manage other CLIs and agents:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;code-writer&lt;/strong&gt; (Gemini-powered): The primary builder. It handles the heavy lifting of implementation and iterates on feedback.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;opencode-code-reviewer&lt;/strong&gt; (Gemini-powered): The "Bridge Agent." This Gemini agent knows how to run the &lt;code&gt;opencode&lt;/code&gt; CLI, capture its feedback, and hand it back to the writer.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;code-reviewer&lt;/strong&gt; (OpenCode-powered): The "Expert Reviewer." This is the native agent inside OpenCode that provides the actual technical critique.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Workflow (Step by Step)
&lt;/h2&gt;

&lt;p&gt;This setup allows me to move from an issue to a verified PR with just two main commands:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Implementation
&lt;/h3&gt;

&lt;p&gt;I start by asking Gemini to implement the feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use the code-writer to implement ISSUE-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;code-writer&lt;/code&gt; generates the initial code, runs local tests, and ensures everything is idiomatic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: The Cross-Model Bridge
&lt;/h3&gt;

&lt;p&gt;Next, I trigger the review. This is where it gets interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use the opencode-code-reviewer to review the 
changes and ask code-writer to address them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, the &lt;strong&gt;Bridge Agent&lt;/strong&gt; does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Executes &lt;code&gt;opencode run --agent code-reviewer&lt;/code&gt; to get a deep-dive analysis.&lt;/li&gt;
&lt;li&gt; Captures the feedback (Status, Summary, Action Items).&lt;/li&gt;
&lt;li&gt; Invokes the &lt;code&gt;code-writer&lt;/code&gt; again, passing the OpenCode feedback as the new instructions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Iterate until Approved
&lt;/h3&gt;

&lt;p&gt;The loop repeats automatically or manually until the OpenCode reviewer returns an "APPROVED" status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Model Diversity&lt;/strong&gt;: Different models have different blind spots. Having a Gemini agent write code and an OpenCode agent review it catches bugs that a single model might miss during self-review.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Automated Orchestration&lt;/strong&gt;: Gemini CLI handles the tool-calling and context-passing. You don't have to copy-paste code into different web UIs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Specialization&lt;/strong&gt;: You use the best tool for each job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Source Code
&lt;/h2&gt;

&lt;p&gt;Here is the core of the setup. You can drop these into your &lt;code&gt;.agents/&lt;/code&gt; folder and customize them for your own models.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;.agents/code-writer.md&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-writer&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;write"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;edit"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bash"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# Code Writer Agent&lt;/span&gt;
You are an expert Google engineer. Implement features, write tests, and address feedback from the reviewer agents.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;code&gt;.agents/opencode-code-reviewer.md&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opencode-code-reviewer&lt;/span&gt;
&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_shell_command"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoke_agent"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# Bridge Agent&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; Run: &lt;span class="sb"&gt;`opencode run --agent code-reviewer "Review changes..."`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Capture output.
&lt;span class="p"&gt;3.&lt;/span&gt; Call &lt;span class="sb"&gt;`code-writer`&lt;/span&gt; with that output to fix any issues.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;code&gt;.agents/code-reviewer.md&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-reviewer&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# Expert Reviewer&lt;/span&gt;
You are an expert Google engineer. Provide a structured review with Status (APPROVED/CHANGES_REQUESTED), Summary, and Action Items.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Leveraging multiple AI tools via a single CLI changed how I build. It’s not about finding the "one" assistant; it's about building the right team.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>ai</category>
      <category>cli</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The Self-Evolving AI Agent: How to Stop Correcting Your LLM Twice</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 19 Apr 2026 21:16:40 +0000</pubDate>
      <link>https://dev.to/onurcinar/the-self-evolving-ai-agent-how-to-stop-correcting-your-llm-twice-15kj</link>
      <guid>https://dev.to/onurcinar/the-self-evolving-ai-agent-how-to-stop-correcting-your-llm-twice-15kj</guid>
      <description>&lt;p&gt;We’ve all been there. You’re working on a lightweight Go microservice. You ask your AI agent to add a simple health-check endpoint.&lt;/p&gt;

&lt;p&gt;The agent responds: &lt;em&gt;"Sure! I'll just install the Gin framework and three middleware libraries..."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You stop it. &lt;em&gt;"No. This is a zero-dependency project. Use &lt;code&gt;net/http&lt;/code&gt; from the standard library."&lt;/em&gt; The agent apologizes, fixes the code, and you move on. But then comes tomorrow. You start a new session, ask for a logging utility, and—lo and behold—it tries to pull in &lt;code&gt;Zap&lt;/code&gt; or &lt;code&gt;Logrus&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Goldfish Effect has struck again.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how to move beyond static prompts and build an AI development environment that &lt;strong&gt;learns from its mistakes&lt;/strong&gt;. By leveraging native memory tools and the concept of "incremental self-evolution," we can force the agent to update its own project memory the moment a correction is made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Case for "Zero-Dependency" Discipline
&lt;/h2&gt;

&lt;p&gt;Why does the "Zero-Dependency" rule matter? It’s the ultimate test for an AI. Most LLMs are trained on vast amounts of boilerplate code that relies on popular frameworks. Their "instinct" is to &lt;code&gt;go get&lt;/code&gt; the world.&lt;/p&gt;

&lt;p&gt;If you are building a high-performance tool or a secure utility, you want to keep your &lt;code&gt;go.mod&lt;/code&gt; clean - like how I am doing it with my side projects &lt;a href="https://github.com/cinar/indicator" rel="noopener noreferrer"&gt;Indicator&lt;/a&gt; and &lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;Resile&lt;/a&gt;. When you force an agent to use the standard library, you aren't just saving disk space; you're enforcing a specific architectural philosophy.&lt;/p&gt;

&lt;p&gt;The goal is to make that philosophy &lt;strong&gt;sticky&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Manual (and Flawed) Way: The End-of-Session Audit
&lt;/h2&gt;

&lt;p&gt;Before we automate this, let's look at how most developers handle this today. At the end of a long coding session, you realize you've corrected the agent half a dozen times. To ensure it doesn't happen again, you might manually ask for an audit:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You:&lt;/strong&gt; &lt;em&gt;"Summarize everything you learned about my preferences today and save it to GEMINI.md."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The agent might then produce something like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prefers &lt;code&gt;net/http&lt;/code&gt; over frameworks like Gin.&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;camelCase&lt;/code&gt; for all internal helper functions.&lt;/li&gt;
&lt;li&gt;Always include a &lt;code&gt;README.md&lt;/code&gt; update for new features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works, but it's fragile. You have to remember to do it. If you're tired or in a rush, you skip the audit. The next morning, you're right back to square one, correcting the same mistakes. It adds friction to the very tool meant to reduce it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: The Proactive Memory Directive
&lt;/h2&gt;

&lt;p&gt;Instead of waiting until the end of a session to "audit" what happened—which breaks your focus and interrupts your flow—you want the agent to be proactive. You don't want to tell the agent what it learned; you want it to &lt;strong&gt;decide&lt;/strong&gt; what was important based on your feedback in real-time.&lt;/p&gt;

&lt;p&gt;Assuming you already use a &lt;code&gt;GEMINI.md&lt;/code&gt; file (or a similar local context file) for your projects, the secret is explicitly authorizing the agent to use its built-in &lt;code&gt;save_memory&lt;/code&gt; tool autonomously. &lt;/p&gt;

&lt;p&gt;By putting a strict directive at the top of your project's memory file, the agent knows it is responsible for its own evolution:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Whenever I correct your behavior, establish a new architectural constraint, or express a coding preference (e.g., 'no dependencies'), you MUST immediately use your &lt;code&gt;save_memory&lt;/code&gt; tool to persist this rule."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, when you correct the agent about that Gin framework, it doesn't just apologize. It silently triggers its tool, updates &lt;code&gt;GEMINI.md&lt;/code&gt; with the new constraint, and &lt;em&gt;then&lt;/em&gt; writes your code. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbs05um0npjvsf7yiz62.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbs05um0npjvsf7yiz62.png" alt=" " width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By putting the burden of synthesis on the AI in real-time, it picks up on nuances you didn't even realize you were enforcing, and it does so seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you're still correcting your AI's basic mistakes every morning, you're treating it like a calculator when you should be treating it like an apprentice.&lt;/p&gt;

&lt;p&gt;We are moving away from "Chatting with AI" and toward &lt;strong&gt;Orchestrating AI Ecosystems&lt;/strong&gt;. By giving your agent a mandate to remember what happened today, it stops being a generic assistant and starts acting like a teammate who has been on the project for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are you handling agent memory? Are you still copying and pasting instructions, or have you set up incremental self-evolution in your project? Let's discuss in the comments.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
      <category>cli</category>
    </item>
    <item>
      <title>Bringing Claude's "Dispatch" Experience to Gemini and OpenCode</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 12 Apr 2026 22:19:26 +0000</pubDate>
      <link>https://dev.to/onurcinar/bringing-claudes-dispatch-experience-to-gemini-and-opencode-3pef</link>
      <guid>https://dev.to/onurcinar/bringing-claudes-dispatch-experience-to-gemini-and-opencode-3pef</guid>
      <description>&lt;p&gt;Claude’s "Dispatch" feature nailed the mobile-to-desktop UX. Being able to pull out your phone, delegate a heavy refactoring task to your local machine, and monitor its progress asynchronously is a massive quality-of-life upgrade. &lt;/p&gt;

&lt;p&gt;But if your daily drivers are CLI-native AI tools like Gemini or OpenCode, you might feel locked out of that seamless remote workflow. Because these tools run in your terminal rather than a proprietary desktop app, they lack a native mobile bridge. &lt;/p&gt;

&lt;p&gt;You don't have to abandon your favorite CLI tools to get that experience. By combining &lt;strong&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;&lt;/strong&gt; and the modern terminal multiplexer &lt;strong&gt;&lt;a href="https://zellij.dev/" rel="noopener noreferrer"&gt;Zellij&lt;/a&gt;&lt;/strong&gt;, you can build a universal "Dispatch" layer. &lt;/p&gt;

&lt;p&gt;The best part? You aren't just sending a fire-and-forget command. You get the exact same interactive, conversational experience on your phone as you do sitting at your mechanical keyboard.&lt;/p&gt;

&lt;p&gt;Here is how to set up your own sovereign, mobile-to-local AI command center.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Missing Link: Zellij Web + Tailscale
&lt;/h2&gt;

&lt;p&gt;The core magic of Claude Dispatch is simply a secure, persistent, remotely accessible session. We can replicate this entirely using open-source infrastructure.&lt;/p&gt;

&lt;p&gt;To bridge the gap between your smartphone browser and your local desktop terminal, we need two components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Tailscale:&lt;/strong&gt; This creates a secure overlay network. We will use &lt;strong&gt;Tailscale&lt;/strong&gt; to safely pipe a local port out to your devices.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Zellij:&lt;/strong&gt; This is the crucial piece. Zellij is a Rust-based terminal multiplexer with a robust &lt;strong&gt;Web Client&lt;/strong&gt;. Unlike SSH apps, which can be clunky on mobile, Zellij renders a fully responsive terminal UI directly in your mobile browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Setting Up Your "Dispatch" Server
&lt;/h2&gt;

&lt;p&gt;On your primary development machine—where your code, compilers, and AI tools live—you need to prepare the web session. Zellij takes privacy seriously, so the web client requires an authentication token and binds strictly to &lt;code&gt;localhost&lt;/code&gt; by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create the Authentication Token
&lt;/h3&gt;

&lt;p&gt;Before starting the web UI, generate your secure login token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zellij web &lt;span class="nt"&gt;--create-token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Make sure to copy and save the outputted token. You will need it to authenticate when you connect from your smartphone.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Start the Zellij Web Server
&lt;/h3&gt;

&lt;p&gt;Next, start the web server and specify the port you want to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zellij web &lt;span class="nt"&gt;--port&lt;/span&gt; 4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Expose the Port via Tailscale
&lt;/h3&gt;

&lt;p&gt;Because Zellij is safely listening only on localhost, you cannot reach it from your phone yet. Instead of exposing this to the public internet, we use Tailscale Serve to proxy that local port exclusively to your private Tailnet.&lt;/p&gt;

&lt;p&gt;Run this in a new terminal tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale serve &lt;span class="nt"&gt;--bg&lt;/span&gt; &lt;span class="nt"&gt;--https&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4000 localhost:4000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Remote Workflow in Action
&lt;/h2&gt;

&lt;p&gt;The real power of this setup is the seamless handoff. You don't need to craft complex, single-shot prompt strings. The interaction is identical to typing directly into your desktop CLI.&lt;/p&gt;

&lt;p&gt;Imagine you are deep into building a Go project. You are sitting at your desk, iterating on some validation logic with OpenCode or Gemini open in your terminal. You realize you need to leave the house, but the task isn't done.&lt;/p&gt;

&lt;p&gt;Here is how the dispatch workflow plays out:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Mobile Handoff
&lt;/h3&gt;

&lt;p&gt;While waiting in line for coffee, you open Chrome or Safari on your phone and navigate to your Tailscale URL (&lt;code&gt;https://my-dev-box.domain.ts.net:4000&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;After pasting in your authentication token, you are instantly dropped right back into your active desktop terminal. You see the exact same interactive AI prompt you were looking at on your monitor moments ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Chat and Dispatch
&lt;/h3&gt;

&lt;p&gt;Because you are in a live, interactive session, you just talk to the CLI naturally. You type into your phone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I need to head out for a bit. Can you run the tests for the checker package, figure out why the struct validation is failing, and apply the fix?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI acknowledges the request and begins its loop—reading your local files, executing &lt;code&gt;go test&lt;/code&gt;, and analyzing the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Detach and Walk Away
&lt;/h3&gt;

&lt;p&gt;This is the "Dispatch" moment. You simply close your mobile browser tab and put your phone in your pocket. &lt;/p&gt;

&lt;p&gt;Because Zellij is managing the session natively on your local hardware, the AI continues to run uninterrupted. It has full access to your local environment to do the heavy lifting.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Asynchronous Monitoring
&lt;/h3&gt;

&lt;p&gt;Check back 20 minutes later. Reopen the URL on your phone, and your terminal state is exactly how you left it. &lt;/p&gt;

&lt;p&gt;If the AI successfully refactored the code and the tests are green, the output is waiting for you. If it ran into a file-permission error, or if OpenCode paused to ask, &lt;em&gt;"Do you want me to commit these changes?"&lt;/em&gt;, the interactive prompt is right there in your mobile browser, patiently waiting for your reply.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The Seamless Return to Desktop
&lt;/h3&gt;

&lt;p&gt;When you finally get back home, the magic of Zellij really shines. You don't have to sync anything, pull down remote cloud changes, or wonder what the AI did while you were gone. You simply sit down at your physical monitor, attach to the running Zellij session, and pick up exactly where you left off. The AI's responses, the shell history, and the code changes are all right there waiting for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Approach Scales
&lt;/h2&gt;

&lt;p&gt;Retrofitting your existing AI workflow with Zellij and Tailscale doesn't just mimic Claude Dispatch; it arguably surpasses it for power users.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agnostic Architecture:&lt;/strong&gt; You aren't locked into one provider's ecosystem. You can use this exact workflow for Gemini, OpenCode, or any future terminal-based AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frictionless UI:&lt;/strong&gt; You don't need a dedicated mobile app or complex SSH key management on your phone. Any modern web browser becomes a window into your live terminal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unrestricted Environment:&lt;/strong&gt; Your AI operates natively. It has full, unrestricted access to your actual development environment—your local databases, Docker containers, and raw file system—without needing to sync cloud workspaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By adding this networking layer, you transform your standard interactive CLIs from desktop-bound tools into true asynchronous agents that travel with you, keeping you in the loop wherever you are.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Prioritize Your Traffic: Priority-Aware Bulkheads in Go</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sun, 12 Apr 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/onurcinar/prioritize-your-traffic-priority-aware-bulkheads-in-go-2ain</link>
      <guid>https://dev.to/onurcinar/prioritize-your-traffic-priority-aware-bulkheads-in-go-2ain</guid>
      <description>&lt;p&gt;Not all traffic is created equal. When your system is under heavy load, should a background cleanup task compete for the same resources as a user's checkout request? &lt;/p&gt;

&lt;p&gt;In a standard bulkhead, the answer is often "yes"—the first 10 requests get in, and the 11th is rejected, regardless of its importance. This is where &lt;strong&gt;Priority-Aware Bulkheads&lt;/strong&gt; come in.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: The "Fairness" Trap
&lt;/h2&gt;

&lt;p&gt;Standard bulkheads are fair. They treat every request the same. But in a real-world system, fairness can be a liability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Critical Traffic&lt;/strong&gt;: User-facing requests (e.g., "Complete Purchase", "Login") that directly impact revenue or user experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard Traffic&lt;/strong&gt;: Regular API calls (e.g., "View Profile", "Search") that are important but not immediately critical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-Priority Traffic&lt;/strong&gt;: Background tasks (e.g., "Generate Report", "Sync Analytics", "Cache Warming") that can be delayed or retried later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When your system is at 90% capacity, you want to stop accepting "Generate Report" requests to ensure there's enough room for "Complete Purchase" calls. A standard bulkhead can't do this; it will fill up with whatever arrives first.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Priority-Aware Bulkheads
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Priority-Aware Bulkhead&lt;/strong&gt; uses &lt;strong&gt;Load Shedding&lt;/strong&gt; based on priority levels. It defines utilization thresholds for different types of traffic. &lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low Priority&lt;/strong&gt;: Allowed only if the bulkhead is less than 50% full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard Priority&lt;/strong&gt;: Allowed only if the bulkhead is less than 80% full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical Priority&lt;/strong&gt;: Allowed until the bulkhead is 100% full.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures that your most important traffic always has a "buffer" of capacity reserved for it, even when the system is under significant pressure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementing with Resile
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;Resile&lt;/a&gt; provides a built-in &lt;code&gt;PriorityBulkhead&lt;/code&gt; that makes this pattern easy to implement.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Define Your Priorities
&lt;/h3&gt;

&lt;p&gt;Resile uses a simple &lt;code&gt;Priority&lt;/code&gt; type with three levels: &lt;code&gt;PriorityLow&lt;/code&gt;, &lt;code&gt;PriorityStandard&lt;/code&gt;, and &lt;code&gt;PriorityCritical&lt;/code&gt;.&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;thresholds&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="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriorityLow&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Shed at 50% utilization&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriorityStandard&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Shed at 80% utilization&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriorityCritical&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Shed only when 100% full&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Create a bulkhead with a capacity of 20&lt;/span&gt;
&lt;span class="n"&gt;pb&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPriorityBulkhead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Attach Priority to Context
&lt;/h3&gt;

&lt;p&gt;You communicate the importance of a request by attaching a priority to its &lt;code&gt;context.Context&lt;/code&gt;.&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;// Create a context with Critical priority&lt;/span&gt;
&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithPriority&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PriorityCritical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Execute the action within the priority bulkhead&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processOrder&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;h3&gt;
  
  
  3. Handle Shedded Load
&lt;/h3&gt;

&lt;p&gt;When a request is rejected because its priority threshold is exceeded, Resile returns &lt;code&gt;resile.ErrShedLoad&lt;/code&gt;. If the bulkhead is physically full (100% capacity), it returns &lt;code&gt;resile.ErrBulkheadFull&lt;/code&gt;.&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrShedLoad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// This low/standard priority request was shedded to save capacity &lt;/span&gt;
    &lt;span class="c"&gt;// for higher-priority traffic.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Use Priority-Aware Bulkheads?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Protect the Critical Path&lt;/strong&gt;: Ensure that your most important business processes remain available even during traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Degradation&lt;/strong&gt;: Instead of a total system failure, your service gracefully degrades by dropping non-essential background work first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better User Experience&lt;/strong&gt;: Users performing critical actions see no slowdown, while background "noise" is managed behind the scenes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Efficiency&lt;/strong&gt;: You don't need to over-provision your infrastructure to handle peak "background" load if you can simply shed it when necessary.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Comparison: Static vs. Priority vs. Adaptive
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Static Bulkhead&lt;/th&gt;
&lt;th&gt;Priority-Aware Bulkhead&lt;/th&gt;
&lt;th&gt;Adaptive Concurrency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Limit Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fixed (e.g., 20)&lt;/td&gt;
&lt;td&gt;Fixed + Thresholds&lt;/td&gt;
&lt;td&gt;Dynamic (Auto-tuned)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic Awareness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (All equal)&lt;/td&gt;
&lt;td&gt;High (Priority-based)&lt;/td&gt;
&lt;td&gt;None (All equal)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple isolation&lt;/td&gt;
&lt;td&gt;Multi-tenant or Tiered apps&lt;/td&gt;
&lt;td&gt;Volatile environments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://dev.to/onurcinar/stop-the-domino-effect-bulkhead-isolation-in-go-5cgl"&gt;Read more about Static Bulkheads&lt;/a&gt; or &lt;a href="https://dev.to/onurcinar/beyond-static-limits-adaptive-concurrency-with-tcp-vegas-in-go-3gne"&gt;Explore Adaptive Concurrency&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Resilience isn't just about keeping the lights on; it's about keeping the &lt;em&gt;right&lt;/em&gt; lights on. Priority-Aware Bulkheads give you the surgical precision needed to manage your system's resources effectively during times of stress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the full example:&lt;/strong&gt; &lt;a href="https://github.com/cinar/resile/tree/main/examples/prioritybulkhead" rel="noopener noreferrer"&gt;Priority Bulkhead Example&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learn more about Resile:&lt;/strong&gt; &lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;github.com/cinar/resile&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>microservices</category>
      <category>distributedsystems</category>
      <category>backend</category>
    </item>
    <item>
      <title>Stopping the Zombie Requests: Distributed Deadline Propagation in Go</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Sat, 11 Apr 2026 15:43:15 +0000</pubDate>
      <link>https://dev.to/onurcinar/stopping-the-zombie-requests-distributed-deadline-propagation-in-go-3ccm</link>
      <guid>https://dev.to/onurcinar/stopping-the-zombie-requests-distributed-deadline-propagation-in-go-3ccm</guid>
      <description>&lt;p&gt;Imagine a common scenario in a microservice architecture: A user clicks a "Buy" button, triggering a request to &lt;strong&gt;Service A&lt;/strong&gt;. Service A calls &lt;strong&gt;Service B&lt;/strong&gt;, which in turn calls &lt;strong&gt;Service C&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suddenly, Service A times out. The user sees an error message and refreshes the page. But &lt;strong&gt;Service B and Service C are still working&lt;/strong&gt; on the original request, consuming CPU, memory, and database connections for a result that will never be seen.&lt;/p&gt;

&lt;p&gt;These are &lt;strong&gt;Zombie Requests&lt;/strong&gt;. In a high-traffic system, they can lead to cascading failures and resource exhaustion, even if the underlying services are technically "healthy."&lt;/p&gt;

&lt;p&gt;To stop the zombies, you need &lt;strong&gt;Distributed Deadline Propagation&lt;/strong&gt;. Here is how to implement it effortlessly using &lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;Resile&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Distributed Deadline Propagation?
&lt;/h2&gt;

&lt;p&gt;Deadlines are not just local timeouts. A deadline represents the &lt;strong&gt;absolute point in time&lt;/strong&gt; after which the entire request chain should be abandoned.&lt;/p&gt;

&lt;p&gt;Distributed Deadline Propagation is the process of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Tracking&lt;/strong&gt; the remaining time (the "budget") as a request moves through the system.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Communicating&lt;/strong&gt; that budget to downstream services via metadata (like HTTP headers).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Aborting early&lt;/strong&gt; if the remaining budget is too small to realistically complete the work.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Resile Way: Smart Deadlines
&lt;/h2&gt;

&lt;p&gt;Resile provides two powerful mechanisms to handle distributed deadlines: &lt;strong&gt;Early Abort&lt;/strong&gt; and &lt;strong&gt;Header Injection&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Early Abort: &lt;code&gt;WithMinDeadlineThreshold&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Why start a request if you only have 2 milliseconds left? The network latency alone will likely exceed that, and you'll just be wasting resources.&lt;/p&gt;

&lt;p&gt;Resile's &lt;code&gt;WithMinDeadlineThreshold&lt;/code&gt; allows you to define a "safety buffer." If the remaining time in the &lt;code&gt;context.Context&lt;/code&gt; is less than this threshold, Resile will &lt;strong&gt;abort the execution immediately&lt;/strong&gt; with a &lt;code&gt;context.DeadlineExceeded&lt;/code&gt; error, before even attempting the work.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/cinar/resile"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Define a policy with a 10ms "Early Abort" threshold.&lt;/span&gt;
&lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithMinDeadlineThreshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// If ctx has only 5ms left, this returns context.DeadlineExceeded instantly.&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&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="n"&gt;apiClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&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;h3&gt;
  
  
  2. Header Injection: &lt;code&gt;InjectDeadlineHeader&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To propagate the deadline to downstream services, you need to "inject" the remaining time into your outgoing requests. Resile provides a transport-agnostic &lt;code&gt;InjectDeadlineHeader&lt;/code&gt; function that supports both standard HTTP and gRPC.&lt;/p&gt;

&lt;h4&gt;
  
  
  For REST/HTTP:
&lt;/h4&gt;

&lt;p&gt;You can inject the remaining milliseconds into a custom header (e.g., &lt;code&gt;X-Request-Timeout&lt;/code&gt;).&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"http://service-b/data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c"&gt;// Inject the remaining milliseconds into the header.&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InjectDeadlineHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"X-Request-Timeout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  For gRPC:
&lt;/h4&gt;

&lt;p&gt;Resile natively supports the standard &lt;code&gt;Grpc-Timeout&lt;/code&gt; header format, ensuring compatibility with the gRPC ecosystem.&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;md&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;

    &lt;span class="c"&gt;// Inject using the gRPC-specific format (e.g., "100m" for 100ms).&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InjectDeadlineHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Grpc-Timeout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewOutgoingContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;md&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grpcClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&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;h2&gt;
  
  
  Why This Matters for Resilience
&lt;/h2&gt;

&lt;p&gt;Without distributed deadlines, your system is vulnerable to &lt;strong&gt;Resource Exhaustion Attacks&lt;/strong&gt;—not from malicious actors, but from your own retries and slow dependencies.&lt;/p&gt;

&lt;p&gt;By implementing propagation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;You save money:&lt;/strong&gt; You're not paying for cloud compute that produces "zombie" results.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You prevent meltdowns:&lt;/strong&gt; Downstream services are protected from "retry storms" that they can't possibly satisfy in time.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;You improve UX:&lt;/strong&gt; Failures happen faster (Fail-Fast), allowing the UI to react or switch to a fallback immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://dev.to/onurcinar/preventing-microservice-meltdowns-adaptive-retries-and-circuit-breakers-in-go-30ho"&gt;Read more: Preventing Meltdowns: How Adaptive Retries Protect Your Downstream&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: Static vs. Distributed Deadlines
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Static Timeouts&lt;/th&gt;
&lt;th&gt;Distributed Deadlines (Resile)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single Service&lt;/td&gt;
&lt;td&gt;Entire Request Chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Awareness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blind to upstream delays&lt;/td&gt;
&lt;td&gt;Aware of the total "time budget"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High waste (Zombie requests)&lt;/td&gt;
&lt;td&gt;Zero waste (Early Abort)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal only&lt;/td&gt;
&lt;td&gt;HTTP/gRPC compatible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Resilience isn't just about making things "work"; it's about knowing when to &lt;strong&gt;stop working&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Distributed Deadline Propagation is the "social contract" of a microservice architecture. It ensures that every service in the chain is working towards a common goal—and respects the reality that sometimes, time simply runs out.&lt;/p&gt;

&lt;p&gt;With Resile, implementing this complex pattern becomes a matter of a few lines of configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore Resile on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;github.com/cinar/resile&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How are you handling request budgets in your distributed systems? Let's discuss!&lt;/p&gt;

</description>
      <category>go</category>
      <category>microservices</category>
      <category>distributedsystems</category>
      <category>performance</category>
    </item>
    <item>
      <title>Native Chaos Engineering: Testing Resilience with Fault &amp; Latency Injection</title>
      <dc:creator>Onur Cinar</dc:creator>
      <pubDate>Fri, 03 Apr 2026 14:51:48 +0000</pubDate>
      <link>https://dev.to/onurcinar/native-chaos-engineering-testing-resilience-with-fault-latency-injection-83</link>
      <guid>https://dev.to/onurcinar/native-chaos-engineering-testing-resilience-with-fault-latency-injection-83</guid>
      <description>&lt;p&gt;You’ve implemented retries, circuit breakers, and timeouts. Your application is now "resilient." But how do you know these policies actually work? Waiting for a production meltdown to verify your configuration is a high-stakes gamble. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native Chaos Engineering&lt;/strong&gt; in Resile allows you to synthetically induce failure and latency directly into your application's execution path, ensuring your resilience policies are battle-tested before they're ever needed in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: "Dark Code" in Resilience Policies
&lt;/h2&gt;

&lt;p&gt;Resilience policies—like retries and circuit breakers—are often "dark code." These are execution paths that are rarely traversed under normal operating conditions. Because they only trigger during failure, they are notoriously difficult to test and prone to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Buggy Configurations&lt;/strong&gt;: A retry limit that is too high, or a circuit breaker threshold that never trips.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Unintended Side Effects&lt;/strong&gt;: A retry loop that accidentally consumes all available database connections.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Silent Failures&lt;/strong&gt;: A fallback strategy that actually panics because it hasn't been executed in months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Traditional chaos engineering tools often operate at the infrastructure layer (e.g., killing pods or dropping network packets). While powerful, these tools can be difficult to set up in local development or staging environments and often lack the granularity to test specific application-level logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Fault &amp;amp; Latency Injection
&lt;/h2&gt;

&lt;p&gt;Resile provides a &lt;strong&gt;Chaos Injector&lt;/strong&gt; middleware that can be integrated directly into any execution policy. By injecting synthetic faults (errors) and latency (delays) with configurable probabilities, you can simulate various failure scenarios without touching your infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Deterministic Randomness&lt;/strong&gt;: Uses Go 1.22's &lt;code&gt;math/rand/v2&lt;/code&gt; for efficient and predictable random number generation.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Context-Aware&lt;/strong&gt;: Latency injection strictly respects &lt;code&gt;context.Context&lt;/code&gt; cancellation. If your request times out while Resile is injecting chaos latency, it exits immediately.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Zero Dependencies&lt;/strong&gt;: Just like the rest of the Resile core, the chaos package depends only on the Go standard library.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Granular Control&lt;/strong&gt;: Configure error and latency probabilities independently for fine-tuned simulation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Practical Usage
&lt;/h2&gt;

&lt;p&gt;Integrating chaos into your existing Resile policies is as simple as adding the &lt;code&gt;WithChaos&lt;/code&gt; option.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Basic Chaos Configuration
&lt;/h3&gt;

&lt;p&gt;You can define a chaos configuration that injects a 10% error rate and adds 100ms of latency to 20% of requests.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/cinar/resile"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/cinar/resile/chaos"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Configure chaos injection&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chaos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ErrorProbability&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="m"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c"&gt;// 10% chance of failure&lt;/span&gt;
    &lt;span class="n"&gt;InjectedError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"chaos!"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c"&gt;// The error to return&lt;/span&gt;
    &lt;span class="n"&gt;LatencyProbability&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c"&gt;// 20% chance of latency&lt;/span&gt;
    &lt;span class="n"&gt;LatencyDuration&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// Delay to inject&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Apply it to an execution&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithChaos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&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;h3&gt;
  
  
  2. Testing Your Circuit Breaker
&lt;/h3&gt;

&lt;p&gt;Chaos injection is exceptionally useful for verifying that your circuit breaker trips under pressure. By setting a high &lt;code&gt;ErrorProbability&lt;/code&gt;, you can force the breaker to transition from &lt;code&gt;Closed&lt;/code&gt; to &lt;code&gt;Open&lt;/code&gt; in a controlled environment.&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;cb&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;circuit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;circuit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;WindowSize&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;           &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;FailureRateThreshold&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c"&gt;// Force 80% error rate to trip the breaker quickly&lt;/span&gt;
&lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;chaos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ErrorProbability&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InjectedError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"synthetic failure"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DoErr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithCircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithChaos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&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="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Circuit Breaker State: %v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c"&gt;// Should be Open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Configuration Reference
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;chaos.Config&lt;/code&gt; struct provides the following options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ErrorProbability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The probability of injecting an error (0.0 to 1.0).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;InjectedError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The error to be returned when an error is injected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LatencyProbability&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;float64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The probability of injecting latency (0.0 to 1.0).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LatencyDuration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;time.Duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The duration of the latency to be injected.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Environment Gating&lt;/strong&gt;: Never enable chaos injection in production unless you are performing a planned game day. Use environment variables to gate the configuration:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ENABLE_CHAOS"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&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;opts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithChaos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loadChaosCfg&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;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Observability&lt;/strong&gt;: Ensure your &lt;code&gt;Instrumenter&lt;/code&gt; (like &lt;code&gt;slog&lt;/code&gt; or &lt;code&gt;OTel&lt;/code&gt;) is active. This allows you to see the injected errors and latencies in your logs and traces, making it easier to verify how your application responds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start Small&lt;/strong&gt;: Begin with low probabilities (e.g., 1-2%) to identify subtle race conditions or timeout issues before increasing the "blast radius."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Resilience is not a "set it and forget it" feature. It requires continuous verification. By bringing chaos engineering directly into your application's execution policies, Resile empowers you to build systems that aren't just theoretically resilient, but practically battle-hardened.&lt;/p&gt;

&lt;p&gt;For more information and advanced usage, visit the &lt;a href="https://github.com/cinar/resile" rel="noopener noreferrer"&gt;github.com/cinar/resile&lt;/a&gt; project.&lt;/p&gt;

</description>
      <category>go</category>
      <category>testing</category>
      <category>sre</category>
      <category>distributedsystems</category>
    </item>
  </channel>
</rss>
