<?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: lkmavi</title>
    <description>The latest articles on DEV Community by lkmavi (@lkmavi).</description>
    <link>https://dev.to/lkmavi</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%2F3997324%2F9e6a7841-8efd-42f2-b674-d9857d52a755.jpg</url>
      <title>DEV Community: lkmavi</title>
      <link>https://dev.to/lkmavi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lkmavi"/>
    <language>en</language>
    <item>
      <title>Fast reflection in Go, without the unsafe surprises — how I built saferefl</title>
      <dc:creator>lkmavi</dc:creator>
      <pubDate>Tue, 23 Jun 2026 11:10:50 +0000</pubDate>
      <link>https://dev.to/lkmavi/fast-reflection-in-go-without-the-unsafe-surprises-how-i-built-saferefl-13ln</link>
      <guid>https://dev.to/lkmavi/fast-reflection-in-go-without-the-unsafe-surprises-how-i-built-saferefl-13ln</guid>
      <description>&lt;p&gt;I needed fast reflection for a project — stdlib &lt;code&gt;reflect&lt;/code&gt; was showing up clearly on the profiler in hot paths like ORM scanning and DI. So I started looking at what already existed, and came across &lt;code&gt;reflect2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The idea there is simple: instead of going through the official &lt;code&gt;reflect&lt;/code&gt; API, the library reads Go's internal struct layout directly. That's fast. Right up until Go changes those internals. And it does — the map rewrite to Swiss Tables in Go 1.24 is a good example. When that happens, the library doesn't crash, doesn't panic, doesn't throw an error. It just quietly reads the wrong bytes. You won't notice right away — maybe a week later, maybe two months later, when weird values start showing up in production and you spend half a day figuring out where they came from.&lt;/p&gt;

&lt;p&gt;That risk didn't sit well with me, so I decided to build the same thing, minus it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;saferefl&lt;/code&gt; aims for the same kind of speed, but through a more honest route: generics, cached field offsets, and an unsafe layer that verifies its own assumptions on startup and falls back to a safe path if something doesn't line up — instead of silently reading memory it shouldn't.&lt;/p&gt;

&lt;p&gt;In practice it looks like this:&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="s"&gt;"github.com/lkmavi/saferefl"&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;User&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="n"&gt;Age&lt;/span&gt;  &lt;span class="kt"&gt;int&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;u&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;User&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Age&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;name&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;saferefl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&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;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// "Alice", nil&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saferefl&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Age"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;31&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;interface{}&lt;/code&gt;, no manual type assertions — the compiler makes sure you're reading a string as a string, not as an int.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's put together
&lt;/h2&gt;

&lt;p&gt;Roughly three layers, stacked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Generic API&lt;/strong&gt; — the part you actually use: &lt;code&gt;Get[T]&lt;/code&gt;/&lt;code&gt;Set[T]&lt;/code&gt;, dot-path support (&lt;code&gt;"Office.City"&lt;/code&gt;) for nested structs, and safe handling of pointers (a nil pointer is an error, not a panic in production).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type info cache&lt;/strong&gt; — the first time you touch a type, its metadata gets built once via stdlib &lt;code&gt;reflect&lt;/code&gt;. After that, everything comes from cache with zero allocations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessor API&lt;/strong&gt; — for the genuinely hot paths. If you're scanning thousands of DB rows per second into structs, you bind the field path once and then access it almost for free:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ageAcc&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;saferefl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MakeAccessor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Age"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;saferefl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnsafePtrOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ageAcc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;// 0.55 ns, zero allocations&lt;/span&gt;
&lt;span class="n"&gt;ageAcc&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="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;31&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Benchmarked on an M3 Max, Go 1.26:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;reflect&lt;/th&gt;
&lt;th&gt;saferefl&lt;/th&gt;
&lt;th&gt;Accessor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Field read&lt;/td&gt;
&lt;td&gt;26.8 ns&lt;/td&gt;
&lt;td&gt;21.2 ns&lt;/td&gt;
&lt;td&gt;0.555 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON decode (per field)&lt;/td&gt;
&lt;td&gt;719 ns&lt;/td&gt;
&lt;td&gt;232 ns&lt;/td&gt;
&lt;td&gt;3.04 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORM scan&lt;/td&gt;
&lt;td&gt;452 ns&lt;/td&gt;
&lt;td&gt;236 ns&lt;/td&gt;
&lt;td&gt;5.51 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The part I'm actually happy with: &lt;code&gt;Accessor&lt;/code&gt; on ORM scanning and struct copying lands within 1.1–1.3× of hand-written code. Basically as close as you'd get to writing &lt;code&gt;u.Age = row.Age&lt;/code&gt; by hand — except it works for any field, looked up by name.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;Get&lt;/code&gt;/&lt;code&gt;Set&lt;/code&gt; already beat &lt;code&gt;reflect.FieldByName&lt;/code&gt; on the very first call. No warm-up loop required to make the library "fast."&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually separates this from reflect2
&lt;/h2&gt;

&lt;p&gt;It's not really about raw speed — the numbers are in the same ballpark. It's about what happens when something doesn't go as expected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reflect2&lt;/code&gt; silently corrupts data in that case. &lt;code&gt;saferefl&lt;/code&gt; checks its layout assumptions at &lt;code&gt;init()&lt;/code&gt; time, and if something doesn't match, it falls back to a safe path instead of reading memory blindly. There are two map backends built in from the start — the old &lt;code&gt;hmap&lt;/code&gt; and the newer Swiss Tables — so the Go 1.24 map rewrite wasn't a surprise, it was something the design already accounted for.&lt;/p&gt;

&lt;p&gt;If you want the full reasoning behind these choices, I wrote it up in &lt;a href="https://github.com/lkmavi/saferefl/discussions/3" rel="noopener noreferrer"&gt;ADR-01&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;p&gt;This is &lt;code&gt;v0.1.0&lt;/code&gt; — an honest MVP. The core API is implemented and tested, CI runs across Go 1.22 through tip, benchmarks regenerate automatically. But the &lt;code&gt;0.x&lt;/code&gt; version isn't just a formality — I'm not ready to promise API stability until it's seen some real usage outside my own projects.&lt;/p&gt;

&lt;p&gt;If you've got a hot path that touches struct fields by name — ORM, DI, serialization — give it a try, I'd genuinely be curious what it looks like on your workload:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/lkmavi/saferefl" rel="noopener noreferrer"&gt;github.com/lkmavi/saferefl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you hit a bug, find an edge case, or just want to share thoughts — issues are open. And a star is always appreciated :)&lt;/p&gt;

</description>
      <category>go</category>
      <category>performance</category>
      <category>reflection</category>
    </item>
  </channel>
</rss>
