<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Corentin GS&apos;s Blog</title><description>I code. I write. I explore.</description><link>https://corentings.dev/</link><language>en</language><atom:link href="https://corentings.dev/atom.xml" rel="self" type="application/atom+xml"/><item><title>Beyond PGN: Designing an Ultra-Efficient Chess Storage Format</title><link>https://corentings.dev/blog/beyond-pgn-chess-storage-format/</link><guid isPermaLink="true">https://corentings.dev/blog/beyond-pgn-chess-storage-format/</guid><description>Every chess database begins with the same question: how do we store a game? PGN is the default answer — human-readable, portable, and surprisingly wasteful. A walkthrough of moving toward binary coordinates and legal-move indexing, and the engineering trade-off between the smallest format and the fastest one.</description><pubDate>Wed, 24 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every chess database begins with a simple question:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do we store a game of chess?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For decades, the default answer has been PGN: Portable Game Notation. It is human-readable, easy to share, easy to edit, and universally supported. A PGN game feels natural because it looks close to how chess players already talk about moves:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. e4 c5 2. Nf3 d6 3. d4 cxd4 4. Nxd4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For humans, this is excellent.&lt;/p&gt;
&lt;p&gt;For machines, it is surprisingly wasteful.&lt;/p&gt;
&lt;p&gt;A chess engine, a database, or a high-performance analysis tool does not need the move to be written as &lt;code&gt;&quot;Nxd4&quot;&lt;/code&gt; or &lt;code&gt;&quot;O-O&quot;&lt;/code&gt;. It does not need spaces, move numbers, dots, or SAN disambiguation. It only needs to know one thing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which move was played?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When I first started thinking about chess storage, I assumed PGN was already compact enough. Then I did the math, and PGN started looking less like an efficient storage format and more like a convenient text format that happens to be compressible.&lt;/p&gt;
&lt;p&gt;If we want to build a modern chess database capable of storing millions of games, loading them instantly, syncing them efficiently, and navigating them without constantly reparsing text, we can go much further.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The First Realization: A Chessboard Is Only 64 Squares&lt;/h2&gt;
&lt;p&gt;A chessboard looks large to a beginner.&lt;/p&gt;
&lt;p&gt;But to a computer, it is tiny.&lt;/p&gt;
&lt;p&gt;There are only 64 squares. That means any square can be represented with just &lt;strong&gt;6 bits&lt;/strong&gt;, because:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2^6 = 64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So instead of storing a move as text like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Nf3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exd5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we can store it as two square coordinates:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from_square -&amp;gt; to_square
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;e2 -&amp;gt; e4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One square needs 6 bits.&lt;/p&gt;
&lt;p&gt;Two squares need:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6 bits + 6 bits = 12 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means most normal chess moves can be represented in only &lt;strong&gt;12 bits&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is the simplest binary approach:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[from: 6 bits][to: 6 bits]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is compact, direct, and fast to decode.&lt;/p&gt;
&lt;p&gt;The reader does not need to parse SAN. It does not need to understand whether &lt;code&gt;&quot;Nbd2&quot;&lt;/code&gt; means the knight from b1 or f3. It does not need to interpret captures, checks, checkmates, or castling notation. It simply reads two square IDs and applies the move.&lt;/p&gt;
&lt;p&gt;The chess rules are still needed to update the board correctly, but the move identity itself is no longer hidden inside text.&lt;/p&gt;
&lt;p&gt;Castling and en passant fit naturally into this scheme. For castling, we encode the king&apos;s from-square to its destination: e1→g1 for kingside, e1→c1 for queenside. The rook move is implied by the rules. For en passant, we encode it as a normal pawn capture using the from-square and the destination square. The capture of the opponent&apos;s pawn is a board-state consequence.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/pgn_1.png&quot; alt=&quot;Diagram showing how PGN text can be replaced by binary coordinates: 6 bits for the source square, 6 bits for the destination square, and 2 extra bits for promotions&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Promotion Problem&lt;/h2&gt;
&lt;p&gt;Chess always has exceptions.&lt;/p&gt;
&lt;p&gt;Most moves can be represented with a starting square and a destination square. But promotions need one extra piece of information.&lt;/p&gt;
&lt;p&gt;If a pawn reaches the final rank, it can promote to one of four pieces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Queen
Rook
Bishop
Knight
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four possibilities require &lt;strong&gt;2 bits&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;00 = queen
01 = rook
10 = bishop
11 = knight
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a promotion move can be stored as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[from: 6 bits][to: 6 bits][promotion: 2 bits]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives us:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12 bits + 2 bits = 14 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So with a simple coordinate-based binary encoding, we can represent almost every chess move in &lt;strong&gt;12 bits&lt;/strong&gt;, and promotion moves in &lt;strong&gt;14 bits&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Compared to PGN text, that is already a major improvement.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why This Is Already Better Than PGN&lt;/h2&gt;
&lt;p&gt;Let&apos;s take a common PGN move:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Nxd4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is four characters. In a normal text encoding, each character usually costs 8 bits.&lt;/p&gt;
&lt;p&gt;So this move alone costs roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4 characters × 8 bits = 32 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that does not include spaces, move numbers, comments, variations, line breaks, or metadata.&lt;/p&gt;
&lt;p&gt;PGN is also more expensive to interpret than its size alone suggests. A PGN parser must deal with disambiguation like &lt;code&gt;&quot;Nbd2&quot;&lt;/code&gt;, &lt;code&gt;&quot;R1e2&quot;&lt;/code&gt;, or &lt;code&gt;&quot;exd5&quot;&lt;/code&gt;. To understand those moves, the parser must reconstruct the board state, find legal pieces that can reach the target square, and determine the correct one. PGN is semantic text that requires chess-aware parsing.&lt;/p&gt;
&lt;p&gt;A coordinate move costs about:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it needs no chess-aware parsing to identify the move. The system reads two square IDs and applies the move directly.&lt;/p&gt;
&lt;p&gt;A rough comparison looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PGN text move:       ~24–40 bits or more
Binary coordinates: ~12–14 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the first big lesson:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PGN was designed for readability and portability, with compactness as a secondary concern.&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Second Realization: Maybe We Don&apos;t Need Coordinates at All&lt;/h2&gt;
&lt;p&gt;We can do better.&lt;/p&gt;
&lt;p&gt;At any given chess position, the number of legal moves is limited.&lt;/p&gt;
&lt;p&gt;A player does not have 4,096 possible moves. They usually have something like 20, 30, 40, or maybe 50 legal moves. The theoretical maximum is 218 legal moves (requiring 8 bits), but such positions are vanishingly rare in practice.&lt;/p&gt;
&lt;p&gt;So instead of storing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from_square -&amp;gt; to_square
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we could do something more clever.&lt;/p&gt;
&lt;p&gt;Imagine that both the writer and the reader generate the exact same list of legal moves for the current position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0: e2e4
1: d2d4
2: g1f3
3: c2c4
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we do not need to store the move itself.&lt;/p&gt;
&lt;p&gt;We only need to store its index.&lt;/p&gt;
&lt;p&gt;If the move played was the third move in the list, we store:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the legal-move-index approach.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Which square did the piece move from, and where did it go?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;we ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Out of all legal moves in this position, which one was chosen?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This can be much smaller.&lt;/p&gt;
&lt;p&gt;If a position has fewer than 64 legal moves, the index fits in &lt;strong&gt;6 bits&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If it has fewer than 128 legal moves, it fits in &lt;strong&gt;7 bits&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Most chess positions fit comfortably in that range.&lt;/p&gt;
&lt;p&gt;So now our rough comparison becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PGN text:           ~24–40 bits per move
Coordinates:        ~12–14 bits per move
Legal move index:   ~6–7 bits per move
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is a dramatic reduction.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/pgn_2.png&quot; alt=&quot;Diagram explaining legal move index encoding: generate legal moves from the current position, store only the selected move index, and use 6 to 7 bits for most positions&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Beautiful Edge Case: Forced Moves Cost Almost Nothing&lt;/h2&gt;
&lt;p&gt;This approach has an elegant consequence: forced moves cost nothing.&lt;/p&gt;
&lt;p&gt;Sometimes a player has only one legal move.&lt;/p&gt;
&lt;p&gt;For example, in a forced checkmate sequence or a position where every move except one is illegal, there may be exactly one valid choice.&lt;/p&gt;
&lt;p&gt;If there is only one legal move, we do not need to store anything.&lt;/p&gt;
&lt;p&gt;The decoder generates the legal move list, sees that only one move exists, and applies it automatically.&lt;/p&gt;
&lt;p&gt;The move costs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 bits
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That feels almost magical.&lt;/p&gt;
&lt;p&gt;But it is not magic. It is simply using the rules of chess as shared context between the encoder and decoder.&lt;/p&gt;
&lt;p&gt;When the rules determine the answer, the file does not need to repeat it.&lt;/p&gt;
&lt;p&gt;This is the core principle behind many efficient encodings:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do not store information that can be reconstructed deterministically.&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Catch: Compression Is Not Free&lt;/h2&gt;
&lt;p&gt;At this point, legal-move indexing sounds like the obvious winner.&lt;/p&gt;
&lt;p&gt;Why store 12 bits when we can store 6?&lt;/p&gt;
&lt;p&gt;Why use coordinates at all?&lt;/p&gt;
&lt;p&gt;The answer is performance.&lt;/p&gt;
&lt;p&gt;To decode a coordinate-based move, the reader does something simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Read from-square
Read to-square
Apply move
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is extremely fast.&lt;/p&gt;
&lt;p&gt;To decode a legal-move index, the reader must do more work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Generate all legal moves
Sort/order them deterministically
Read the index
Select the matching move
Apply move
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That means every move requires legal move generation.&lt;/p&gt;
&lt;p&gt;For a single game, this is fine.&lt;/p&gt;
&lt;p&gt;For millions of games, it becomes expensive.&lt;/p&gt;
&lt;p&gt;Compression is only half the problem. A chess database is also a performance problem.&lt;/p&gt;
&lt;p&gt;If your format is beautifully small but slow to scan, slow to open, slow to index, or difficult to randomly access, it may be worse in practice than a larger format.&lt;/p&gt;
&lt;p&gt;This gives us the fundamental trade-off:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Smaller files usually require more computation.
Faster decoding usually requires storing more explicit information.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no universal best answer. There is only the best answer for a specific system.&lt;/p&gt;
&lt;h3&gt;Coordinates vs Legal Move Indexes&lt;/h3&gt;
&lt;p&gt;The coordinate approach is simple, direct, and practical.&lt;/p&gt;
&lt;p&gt;It gives us:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[from square][to square][optional promotion]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Its strengths are obvious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Very fast to decode.&lt;/li&gt;
&lt;li&gt;Easy to implement.&lt;/li&gt;
&lt;li&gt;Easy to validate.&lt;/li&gt;
&lt;li&gt;Good for random access.&lt;/li&gt;
&lt;li&gt;Good for database indexing.&lt;/li&gt;
&lt;li&gt;Does not require generating all legal moves just to identify the move.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Its weakness is that it is not maximally compact.&lt;/p&gt;
&lt;p&gt;The legal-move-index approach is more compressed.&lt;/p&gt;
&lt;p&gt;It gives us:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[index of move in legal move list]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Its strengths are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extremely compact.&lt;/li&gt;
&lt;li&gt;Can exploit forced moves.&lt;/li&gt;
&lt;li&gt;Can approach the theoretical information needed to describe a chess game.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Its weaknesses are serious:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Requires legal move generation at every ply.&lt;/li&gt;
&lt;li&gt;Requires a perfectly deterministic move ordering.&lt;/li&gt;
&lt;li&gt;Harder to implement safely.&lt;/li&gt;
&lt;li&gt;More expensive to decode at massive scale.&lt;/li&gt;
&lt;li&gt;More fragile if the rules, variants, or encoding assumptions change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That narrows the question to: what does the system need to optimize for?&lt;/p&gt;
&lt;p&gt;A serious chess database needs to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Import millions of games.&lt;/li&gt;
&lt;li&gt;Open individual games instantly.&lt;/li&gt;
&lt;li&gt;Jump to arbitrary positions.&lt;/li&gt;
&lt;li&gt;Build position indexes.&lt;/li&gt;
&lt;li&gt;Search references.&lt;/li&gt;
&lt;li&gt;Display games quickly.&lt;/li&gt;
&lt;li&gt;Sync data efficiently.&lt;/li&gt;
&lt;li&gt;Support annotations, comments, variations, and repertoires.&lt;/li&gt;
&lt;li&gt;Export back to PGN when needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For that kind of system, raw compactness is not enough. A format must also be operationally useful.&lt;/p&gt;
&lt;p&gt;A coordinate-based binary format is often the better engineering compromise. It is still far smaller than PGN, but it remains simple and fast.&lt;/p&gt;
&lt;p&gt;For cold archival storage where decoding speed matters less, legal-move indexing is more attractive. The file would be tiny, and we could afford extra computation because the data would rarely be read.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/pgn_3.png&quot; alt=&quot;Comparison chart showing the storage and decoding trade-off between PGN text, binary coordinates, and legal move indexing&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Practical Design Choice&lt;/h2&gt;
&lt;p&gt;For a modern chess database, I would start with coordinate-based binary moves.&lt;/p&gt;
&lt;p&gt;Something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12 bits for normal moves
14 bits for promotions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives an excellent balance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Much smaller than PGN
Much faster to parse than PGN
Simple enough to implement safely
Efficient for massive databases
Friendly to random access and indexing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Legal-move indexing can still be useful later, especially for a compressed distribution format or cold storage format.&lt;/p&gt;
&lt;p&gt;But for the primary editable database representation, coordinates are a better foundation.&lt;/p&gt;
&lt;p&gt;This leads to a layered architecture:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Internal database format: coordinate-based binary moves
Optional archive format: legal-move-index compression
Export format: deterministic PGN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That way, each format does what it is best at.&lt;/p&gt;
&lt;p&gt;PGN remains the universal language for humans and existing tools.&lt;/p&gt;
&lt;p&gt;The binary coordinate format becomes the fast internal representation.&lt;/p&gt;
&lt;p&gt;The legal-move-index format becomes an optional compression layer for distribution or archival storage.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/pgn_4.png&quot; alt=&quot;Architecture diagram for modern chess storage: PGN import and export, binary coordinate core, optional compression layer, compact initial state, and database features&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What About the Initial Position?&lt;/h2&gt;
&lt;p&gt;So far, we assumed that every game starts from the normal chess starting position.&lt;/p&gt;
&lt;p&gt;But real databases also contain games that begin from custom positions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chess960 games.&lt;/li&gt;
&lt;li&gt;Training positions.&lt;/li&gt;
&lt;li&gt;Composed studies.&lt;/li&gt;
&lt;li&gt;Partial game fragments.&lt;/li&gt;
&lt;li&gt;Engine lines.&lt;/li&gt;
&lt;li&gt;Positions imported from FEN.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For those cases, the format needs to store the initial board state.&lt;/p&gt;
&lt;p&gt;The usual text format for this is FEN:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FEN is useful and readable, but again, it is text.&lt;/p&gt;
&lt;p&gt;A binary format could store a compact &quot;Bit-FEN&quot; instead.&lt;/p&gt;
&lt;p&gt;At minimum, an initial position needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Piece placement.&lt;/li&gt;
&lt;li&gt;Side to move.&lt;/li&gt;
&lt;li&gt;Castling rights.&lt;/li&gt;
&lt;li&gt;En passant square, if any.&lt;/li&gt;
&lt;li&gt;Halfmove clock.&lt;/li&gt;
&lt;li&gt;Fullmove number.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The exact encoding can vary, but the principle is the same:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Store chess state as structured binary data, not as text that must be reparsed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For normal games, we can omit the initial board entirely and assume the standard starting position.&lt;/p&gt;
&lt;p&gt;For custom games, we include a compact binary initial-state block.&lt;/p&gt;
&lt;p&gt;That gives us efficiency without losing generality.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion: PGN Is the Beginning, Not the End&lt;/h2&gt;
&lt;p&gt;PGN gave chess software a common language.&lt;/p&gt;
&lt;p&gt;But modern chess software can go beyond it.&lt;/p&gt;
&lt;p&gt;A binary chess format can represent moves in 12 to 14 bits using coordinates. With legal-move indexing, it can sometimes approach 6 to 7 bits per move, or even 0 bits for forced moves.&lt;/p&gt;
&lt;p&gt;But the smallest format is not always the best format.&lt;/p&gt;
&lt;p&gt;For an interactive chess database, the best design is likely a pragmatic one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Use binary coordinates for speed.
Use legal-move indexing where compression matters.
Use PGN for import and export.
Use a compact binary position format for custom starting states.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives us a layered system where each format plays to its strength:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Human compatibility.
Machine efficiency.
Fast random access.
Compact storage.
Deterministic export.
Room for future compression.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PGN remains useful.&lt;/p&gt;
&lt;p&gt;But it does not have to be where the story ends.&lt;/p&gt;
&lt;p&gt;Sometimes the real breakthrough begins when we stop asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How do we store the notation?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And start asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What is the smallest useful truth the machine actually needs?&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Beyond Minimalism: A Japanese-Inspired UX for the Web</title><link>https://corentings.dev/blog/ux-japan-3/</link><guid isPermaLink="true">https://corentings.dev/blog/ux-japan-3/</guid><description>The final UX Japan article: how ma, bento layouts, and designed density can give web interfaces more context, comparison, and confidence.</description><pubDate>Fri, 19 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After looking at why Japanese websites often feel overloaded, and what Western UX can learn from them, the obvious question is simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Should Japanese websites become minimalist?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I do not think so.&lt;/p&gt;
&lt;p&gt;Western minimalism solves real problems: focus, hierarchy, less noise, faster pages. All good. But it can also become an aesthetic reflex: remove the text, hide the details, simplify the comparison, make the action bigger, trust the brand mood to carry the rest.&lt;/p&gt;
&lt;p&gt;That works until the user needs context. As the &lt;a href=&quot;/blog/ux-japan-1/&quot;&gt;first article in this series&lt;/a&gt; argued, density can be information, navigation, promotion, comparison, reassurance, or visible service — and the &lt;a href=&quot;/blog/ux-japan-2/&quot;&gt;second article&lt;/a&gt; showed what Western UX can borrow from that.&lt;/p&gt;
&lt;p&gt;Japanese web design does not need Western minimalism. And Western UX does not need to absorb Japanese density wholesale. The more interesting future sits between them: interfaces that preserve context, reassurance, comparison, and visible service, but organize them with hierarchy, spacing, expandable details, and accessibility.&lt;/p&gt;
&lt;p&gt;Clutter is a problem of structure, not of quantity.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Accumulated Density vs Designed Density&lt;/h2&gt;
&lt;p&gt;The first mistake is treating density as one thing.&lt;/p&gt;
&lt;p&gt;Some density carries real value. Delivery conditions, cancellation terms, official notices, price comparisons, support paths, campaign deadlines, and compatibility details can all reduce uncertainty. They are part of the service.&lt;/p&gt;
&lt;p&gt;Other density is only organizational sediment. A department adds a banner, a campaign demands a module, a legal warning becomes permanent, and an internal stakeholder protects a link. Over time, the page records company politics instead of user needs.&lt;/p&gt;
&lt;p&gt;That is accumulated density.&lt;/p&gt;
&lt;p&gt;Designed density starts from a different question: if the user really needs this information, where should it live, how should it be grouped, and when should it appear?&lt;/p&gt;
&lt;p&gt;Accumulated density is what happens when a page records company politics. Designed density is what happens when a page records user needs. Here is the difference:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Accumulated density&lt;/th&gt;
&lt;th&gt;Designed density&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Everything competes&lt;/td&gt;
&lt;td&gt;Clear priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Banners everywhere&lt;/td&gt;
&lt;td&gt;Modules by user intent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Links repeated randomly&lt;/td&gt;
&lt;td&gt;Redundancy used deliberately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trust cues scattered&lt;/td&gt;
&lt;td&gt;Trust cues near decision points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense because nobody removes&lt;/td&gt;
&lt;td&gt;Dense because users need context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The point is to make information legible enough that density can do its job — not to make dense pages look sparse.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Ma Is the Interval Between Things&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Ma (間) in design&lt;/strong&gt; is the meaningful interval between elements that lets each one make sense in relation to the others. It is relational spacing: the pause that gives a page room to think.&lt;/p&gt;
&lt;p&gt;In Western design conversations, ma often gets flattened into a nicer word for whitespace. That misses the point.&lt;/p&gt;
&lt;p&gt;Ma is relational space. It is the interval that lets one thing make sense beside another thing.&lt;/p&gt;
&lt;p&gt;In interface design, ma can be the space between decision groups. It can be the pause before a high-commitment action. It can separate a promotion from a functional tool. It can give the user a moment to understand why a trust cue is near a button, why a comparison table follows a promise, or why support information appears before checkout.&lt;/p&gt;
&lt;p&gt;Intervals make density sustainable. Enough information makes simplicity trustworthy. A page needs both — and this is exactly the case for &lt;a href=&quot;/blog/ux-japan-2/&quot;&gt;trustful minimalism&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Many pages fail in opposite ways. One makes every element shout. Another leaves the user hunting for basic facts. Ma offers another path: enough separation to think, without pretending that users only need one sentence and a button.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bento as an Interface Model&lt;/h2&gt;
&lt;p&gt;Ma gives density rhythm. Bento gives it structure.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-3-2.png&quot; alt=&quot;A bento-style web layout with compact, clearly bordered modules for action, value proposition, trust, comparison, campaign, FAQ, and support — variety held in place by structure, not by emptiness&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bento structure as an interface model: many modules, each with a clear job, held in place by visible rhythm and ma.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A bento box gives each item a place. Rice, fish, vegetables, pickles, and sauce can sit together because the container gives the variety order. Nothing needs to become the same thing to belong.&lt;/p&gt;
&lt;p&gt;A Japanese-inspired bento interface might include these modules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A primary action&lt;/li&gt;
&lt;li&gt;A short value proposition&lt;/li&gt;
&lt;li&gt;A trust strip&lt;/li&gt;
&lt;li&gt;A comparison block&lt;/li&gt;
&lt;li&gt;A campaign block&lt;/li&gt;
&lt;li&gt;A FAQ preview&lt;/li&gt;
&lt;li&gt;A support path&lt;/li&gt;
&lt;li&gt;Compatibility or expert details&lt;/li&gt;
&lt;li&gt;A plain-language entry point for first-time users&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The page is a set of compact zones, each with a job: decide here, compare here, verify here, learn here, ask for help here.&lt;/p&gt;
&lt;p&gt;This pattern fits complex products especially well. Insurance, travel, and government services rarely fit into a pure landing-page funnel. Users need to compare, hesitate, check conditions, and return later, and a bento structure respects that without turning the page into a wall.&lt;/p&gt;
&lt;h3&gt;Shogi, Go, and Relational Meaning&lt;/h3&gt;
&lt;p&gt;Board games offer a useful metaphor, as long as it stays a metaphor.&lt;/p&gt;
&lt;p&gt;Chess is easy to read as focal-object design: distinct shapes, explicit identities, direct tactical roles. As pieces disappear, the board often becomes simpler.&lt;/p&gt;
&lt;p&gt;Go works differently. The stones are visually identical, and their meaning comes from relationship: territory, influence, proximity, pressure, and future possibility. Shogi adds another kind of density because captured pieces can return to the board. The game state can recompose instead of only simplifying.&lt;/p&gt;
&lt;p&gt;That proves nothing about interfaces, but it is a useful way to think.&lt;/p&gt;
&lt;p&gt;A Western landing page isolates one action. A Japanese service page preserves a field of possible actions. A good hybrid keeps the field visible and makes the next move legible. The lesson from Shogi is relational meaning: the surrounding field changes the value of each move.&lt;/p&gt;
&lt;p&gt;For UX, the button is not the whole story. A &quot;Start&quot; button beside a vague promise feels different from a &quot;Start&quot; button beside pricing, cancellation terms, support availability, setup time, and proof that the service is official.&lt;/p&gt;
&lt;p&gt;The object is the same. The field changes the decision.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Layered Information UX&lt;/h2&gt;
&lt;p&gt;The practical pattern I would take from all this is &lt;strong&gt;layered information UX&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Layered information gives immediate reassurance, visible structure, and deeper detail on demand. It decides what the user needs now, what they may need next, and what should remain available without dominating the page.&lt;/p&gt;
&lt;p&gt;That can look like:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Short trust cues near the main call to action&lt;/li&gt;
&lt;li&gt;Expandable delivery, pricing, return, and compatibility details&lt;/li&gt;
&lt;li&gt;Beginner and expert paths&lt;/li&gt;
&lt;li&gt;Comparison cards before long technical tables&lt;/li&gt;
&lt;li&gt;Sticky summaries for dense forms or product pages&lt;/li&gt;
&lt;li&gt;Campaign modules visually separate from decision-critical information&lt;/li&gt;
&lt;li&gt;Semantic HTML instead of image-based text, so density is accessible to screen readers, search, and translation&lt;/li&gt;
&lt;li&gt;Keyboard- and zoom-friendly dense layouts, designed from the start rather than patched on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is where Japanese density and Western minimalism can stop arguing.&lt;/p&gt;
&lt;p&gt;Minimalism brings priority and focus. Japanese information-rich design adds what many modern interfaces underplay: context, reassurance, room to explore, and visible service. Layered UX combines them by asking the more precise question: what information should be visible at this moment of trust?&lt;/p&gt;
&lt;p&gt;Not all information deserves equal weight. Some information deserves to be seen before the user commits.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Trustful Minimalism Looks Like in Practice&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/blog/ux-japan-2/&quot;&gt;Article 2&lt;/a&gt; introduced the term &lt;strong&gt;trustful minimalism&lt;/strong&gt;. It keeps the visual discipline of modern UX but restores the information users need to feel confident.&lt;/p&gt;
&lt;p&gt;In practice, it looks like a few small choices repeated consistently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A trust strip beside the main action, not buried in the footer&lt;/li&gt;
&lt;li&gt;Comparison options visible before commitment&lt;/li&gt;
&lt;li&gt;Reassurance close to the decision: price, cancellation, delivery, support, security, return policy&lt;/li&gt;
&lt;li&gt;Expandable details for users who want to dig deeper&lt;/li&gt;
&lt;li&gt;Separation between promotional modules and decision-critical modules&lt;/li&gt;
&lt;li&gt;A bento structure for complex products rather than a single hero block&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It also looks like restraint: remove the elements that do not help a user decide, trust, or compare. Keep the ones that do.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Western Designers Can Borrow&lt;/h2&gt;
&lt;p&gt;Western designers should not copy Japanese visual density as a style. Copying the surface gives you banners, badges, mascots, and crowded grids without the cultural or service logic that made them useful. Article 2 walks through &lt;a href=&quot;/blog/ux-japan-2/&quot;&gt;what Western UX can borrow in detail&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Worth borrowing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bento structure for complex products&lt;/li&gt;
&lt;li&gt;Trust cues near calls to action&lt;/li&gt;
&lt;li&gt;Visible comparison instead of pure persuasion&lt;/li&gt;
&lt;li&gt;Exploration paths, not only funnels&lt;/li&gt;
&lt;li&gt;Ma as rhythm, not decoration&lt;/li&gt;
&lt;li&gt;Progressive disclosure that does not hide legitimacy&lt;/li&gt;
&lt;li&gt;Character or mascot guidance only when it helps the task&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Skip these:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Random banner competition&lt;/li&gt;
&lt;li&gt;Image-based text&lt;/li&gt;
&lt;li&gt;Every stakeholder on the homepage&lt;/li&gt;
&lt;li&gt;Duplicate links without logic&lt;/li&gt;
&lt;li&gt;Density without hierarchy&lt;/li&gt;
&lt;li&gt;Decorative reassurance that does not answer a real user concern&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;a href=&quot;/blog/ux-japan-1/&quot;&gt;first article reframes this question around what the density is doing&lt;/a&gt;. The better question is more specific: what does the user need to understand, trust, compare, and choose?&lt;/p&gt;
&lt;p&gt;If the information does not help with one of those jobs, remove it. If it does help, design a place for it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Enough Context, Enough Room&lt;/h2&gt;
&lt;p&gt;In the first article, density was the object of suspicion: maybe those crowded pages were not only clutter, but context, navigation, promotion, comparison, reassurance, or visible service.&lt;/p&gt;
&lt;p&gt;The second article turned that suspicion back toward Western UX. Simplicity is useful, but it can also erase the information users need to feel confident.&lt;/p&gt;
&lt;p&gt;This final article is where I land: the future is structured, contextual, and trust-aware.&lt;/p&gt;
&lt;p&gt;The future of UX belongs to interfaces that understand why information is there. Japanese web design reminds us that users need context, confidence, comparison, and visible service readiness — not just a button. Western UX reminds us that attention is limited and hierarchy matters.&lt;/p&gt;
&lt;p&gt;The interface that earns trust gives people enough context to decide and enough room to change their mind.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References and Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Stanford Encyclopedia of Philosophy, &lt;em&gt;Japanese Aesthetics&lt;/em&gt; (Ma, Wabi-sabi, related concepts)&lt;/li&gt;
&lt;li&gt;Tuch, A. N., et al. (2009), &lt;em&gt;Visual complexity of websites: Effects on users&apos; experience, physiology, mood, and behavior&lt;/em&gt;, Human-Computer Interaction&lt;/li&gt;
&lt;li&gt;Nordhoff, August, Oliveira, and Reinecke, &lt;em&gt;A Case for Design Localization&lt;/em&gt; (CHI 2018)&lt;/li&gt;
&lt;li&gt;Baughan et al., studies on visual complexity, website search, and cross-cultural attention&lt;/li&gt;
&lt;li&gt;Nisbett and Masuda, &lt;em&gt;Culture and Point of View&lt;/em&gt; and &lt;em&gt;Attending Holistically Versus Analytically&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;W3C, &lt;em&gt;Requirements for Japanese Text Layout&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Japan House Los Angeles, &lt;em&gt;A Perspective on the Japanese Concept of Ma&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;For the series: &lt;a href=&quot;/blog/ux-japan-1/&quot;&gt;Why Japanese Websites Look Overloaded: Density, Tokyo, and Trust&lt;/a&gt; and &lt;a href=&quot;/blog/ux-japan-2/&quot;&gt;What Western UX Can Learn from Japanese Web Design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Context and Cancellation in Go: A Practical Guide</title><link>https://corentings.dev/blog/go-context-cancellation/</link><guid isPermaLink="true">https://corentings.dev/blog/go-context-cancellation/</guid><description>Learn how Go context cancellation works in real systems: timeouts, request lifecycles, errgroup fan-out, and graceful shutdown without leaked goroutines.</description><pubDate>Wed, 17 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every goroutine you spawn is a promise that something will eventually stop. If you don&apos;t control when that happens, you leak intention: your program keeps doing work nobody asked for, long after the caller moved on.&lt;/p&gt;
&lt;p&gt;Go&apos;s &lt;code&gt;context&lt;/code&gt; package is the standard answer. But like most standard answers, it&apos;s easy to use poorly and hard to use well. This builds on the channel patterns from &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;the worker pool article&lt;/a&gt; and &lt;a href=&quot;/blog/go-pattern-pipeline/&quot;&gt;the pipeline article&lt;/a&gt;: once goroutines compose, cancellation becomes part of the design, not a cleanup detail.&lt;/p&gt;
&lt;h2&gt;The Problem: Orphaned Work&lt;/h2&gt;
&lt;p&gt;Imagine a handler that queries three databases in parallel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func handleRequest(w http.ResponseWriter, r *http.Request) {
    var wg sync.WaitGroup
    wg.Add(3)

    go func() { defer wg.Done(); queryDB1() }()
    go func() { defer wg.Done(); queryDB2() }()
    go func() { defer wg.Done(); queryDB3() }()

    wg.Wait()
    // ... respond
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens when the client disconnects after 50ms? The goroutines keep running. The databases keep working. Resources burn for a request that will never be answered.&lt;/p&gt;
&lt;p&gt;This is the orphaned work problem. And it&apos;s everywhere in concurrent code.&lt;/p&gt;
&lt;h2&gt;What Context Gives You&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;context.Context&lt;/code&gt; is a request-scoped value that carries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cancellation signals&lt;/strong&gt; — tell goroutines to stop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deadlines&lt;/strong&gt; — automatic cancellation after a timeout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key-value pairs&lt;/strong&gt; — request-scoped metadata (trace IDs, user info)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key insight: context flows &lt;em&gt;down&lt;/em&gt;. You create it at the top of a request, pass it to every function and goroutine, and everything respects the same lifecycle.&lt;/p&gt;
&lt;h2&gt;The Three Flavors&lt;/h2&gt;
&lt;h3&gt;1. &lt;code&gt;context.Background()&lt;/code&gt; — The Root&lt;/h3&gt;
&lt;p&gt;Use this at the top level: &lt;code&gt;main&lt;/code&gt;, test setup, initialization. It never cancels. Everything else derives from it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctx := context.Background()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. &lt;code&gt;context.WithCancel()&lt;/code&gt; — Manual Control&lt;/h3&gt;
&lt;p&gt;When you need to say &quot;stop now&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always call this

// pass ctx to goroutines
// call cancel() when you&apos;re done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;cancel&lt;/code&gt; function is idempotent. Call it once, twice, a hundred times—same result. This is important for &lt;code&gt;defer&lt;/code&gt; patterns.&lt;/p&gt;
&lt;h3&gt;3. &lt;code&gt;context.WithTimeout()&lt;/code&gt; / &lt;code&gt;WithDeadline()&lt;/code&gt; — Time-Bound&lt;/h3&gt;
&lt;p&gt;When you need to say &quot;stop after 2 seconds&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The context automatically cancels when the deadline hits. You still call &lt;code&gt;cancel()&lt;/code&gt; in defer to release resources early if you finish sooner.&lt;/p&gt;
&lt;h2&gt;Listening for Cancellation&lt;/h2&gt;
&lt;p&gt;A context doesn&apos;t stop goroutines by force. It asks politely. You have to listen:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func queryWithContext(ctx context.Context, db *sql.DB) error {
    rows, err := db.QueryContext(ctx, &quot;SELECT ...&quot;)
    if err != nil {
        return err
    }
    defer rows.Close()

    for rows.Next() {
        select {
        case &amp;lt;-ctx.Done():
            return ctx.Err() // context cancelled
        default:
            // process row
        }
    }
    return rows.Err()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ctx.Done()&lt;/code&gt; channel closes when cancellation happens. Checking it in a &lt;code&gt;select&lt;/code&gt; gives your goroutine a clean exit path.&lt;/p&gt;
&lt;h2&gt;The Pattern: Per-Request Context&lt;/h2&gt;
&lt;p&gt;In HTTP handlers, the request already carries a context:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // use this!

    // add timeout
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    result, err := service.Process(ctx, r.Body)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, &quot;timeout&quot;, http.StatusGatewayTimeout)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(result)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how we derive from &lt;code&gt;r.Context()&lt;/code&gt; instead of &lt;code&gt;context.Background()&lt;/code&gt;. This way, if the client disconnects, our timeout context inherits that cancellation.&lt;/p&gt;
&lt;h2&gt;Propagation: The Golden Rule&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Every function that does I/O or spawns goroutines should take &lt;code&gt;context.Context&lt;/code&gt; as its first parameter.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the convention. Follow it even when you don&apos;t think you need it. The caller knows their constraints better than you do.&lt;/p&gt;
&lt;p&gt;Bad:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func FetchUser(userID string) (*User, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Good:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func FetchUser(ctx context.Context, userID string) (*User, error)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Goroutines and Context&lt;/h2&gt;
&lt;p&gt;When you spawn a goroutine, pass it the context:&lt;/p&gt;
&lt;p&gt;For fan-out work, &lt;code&gt;golang.org/x/sync/errgroup&lt;/code&gt; gives you cancellation and error propagation without hand-rolling channel bookkeeping:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get golang.org/x/sync/errgroup
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;func fetchAll(ctx context.Context, urls []string) ([]Result, error) {
    g, ctx := errgroup.WithContext(ctx)

    results := make([]Result, len(urls))

    for i, url := range urls {
        idx, u := i, url
        g.Go(func() error {
            res, err := fetch(ctx, u)
            if err != nil {
                return err
            }
            results[idx] = res
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }

    return results, nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Derive the child context with &lt;code&gt;errgroup.WithContext&lt;/code&gt; so we can stop siblings on error&lt;/li&gt;
&lt;li&gt;Pass &lt;code&gt;ctx&lt;/code&gt; to &lt;code&gt;fetch()&lt;/code&gt; so it can respect cancellation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;g.Wait()&lt;/code&gt; waits for every goroutine and returns the first error&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Common Mistakes&lt;/h2&gt;
&lt;h3&gt;1. Storing Context in Structs&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Service struct {
    ctx context.Context // DON&apos;T
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Context is for function parameters, not struct fields. It makes the lifetime unclear.&lt;/p&gt;
&lt;h3&gt;2. Ignoring &lt;code&gt;ctx.Err()&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;select {
case &amp;lt;-ctx.Done():
    return nil // what happened?
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Better:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;case &amp;lt;-ctx.Done():
    return fmt.Errorf(&quot;fetch interrupted: %w&quot;, ctx.Err())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Not Checking Cancellation in Tight Loops&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for i := 0; i &amp;lt; 1e9; i++ {
    // heavy computation
    // never checks ctx.Done()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check periodically:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for i := 0; i &amp;lt; 1e9; i++ {
    if i % 1000 == 0 {
        select {
        case &amp;lt;-ctx.Done():
            return ctx.Err()
        default:
        }
    }
    // heavy computation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Creating Background Contexts Deep in the Stack&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func helper() {
    ctx := context.Background() // wrong
    db.QueryContext(ctx, ...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Always accept context from the caller:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func helper(ctx context.Context) {
    db.QueryContext(ctx, ...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Values: Use Sparingly&lt;/h2&gt;
&lt;p&gt;Context values carry request-scoped metadata:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type contextKey string

const traceIDKey contextKey = &quot;traceID&quot;

ctx = context.WithValue(ctx, traceIDKey, traceID)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Access them with a type check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;traceID, ok := ctx.Value(traceIDKey).(string)
if !ok {
    return errors.New(&quot;missing trace ID&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Rules for values:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use for &lt;em&gt;request-scoped&lt;/em&gt; data (trace IDs, auth tokens), not &lt;em&gt;application&lt;/em&gt; data&lt;/li&gt;
&lt;li&gt;Use typed keys to avoid collisions: &lt;code&gt;type contextKey string&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Don&apos;t use as a replacement for function parameters&lt;/li&gt;
&lt;li&gt;Document what keys your function expects&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Graceful Shutdown&lt;/h2&gt;
&lt;p&gt;The ultimate cancellation pattern: shutting down a server cleanly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    srv := &amp;amp;http.Server{
        Addr:    &quot;:8080&quot;,
        Handler: http.HandlerFunc(handler),
    }

    go func() {
        &amp;lt;-ctx.Done()
        shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
        srv.Shutdown(shutdownCtx)
    }()

    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatal(err)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;signal.NotifyContext&lt;/code&gt; creates a context that cancels on Ctrl+C. We use that to trigger a graceful shutdown with its own timeout.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Always derive from the caller&apos;s context&lt;/strong&gt; — never create &lt;code&gt;Background()&lt;/code&gt; deep in the stack&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pass context as the first parameter&lt;/strong&gt; — make it obvious and consistent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check &lt;code&gt;ctx.Done()&lt;/code&gt; in loops and selects&lt;/strong&gt; — give your goroutines an exit&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call &lt;code&gt;cancel()&lt;/code&gt; in defer&lt;/strong&gt; — clean up resources, prevent leaks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use &lt;code&gt;WithTimeout&lt;/code&gt; for external calls&lt;/strong&gt; — protect against slow dependencies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use values sparingly&lt;/strong&gt; — they&apos;re convenient but opaque&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Context is about &lt;em&gt;scope&lt;/em&gt;: every request gets a boundary, every goroutine lives within it, and when the boundary closes, everything inside knows to stop.&lt;/p&gt;
&lt;p&gt;That&apos;s how you build systems that don&apos;t leak: memory, connections, or intentions.&lt;/p&gt;
</content:encoded></item><item><title>How to Merge PGN Files in F#: Streaming, Performance, and Discriminated Unions</title><link>https://corentings.dev/blog/merge-pgn-files-fsharp/</link><guid isPermaLink="true">https://corentings.dev/blog/merge-pgn-files-fsharp/</guid><description>How I built a CLI tool to merge chess PGN files using F#&apos;s type system, streaming I/O, and functional patterns — merging gigabytes of games with 64 KB of memory.</description><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I needed to merge hundreds of Lichess PGN files into a single database for opening preparation. The existing tools either required Python dependencies, discarded game metadata, or merged games into variation trees rather than concatenating them cleanly. I wanted a standalone CLI that takes a folder of &lt;code&gt;.pgn&lt;/code&gt; files and produces one &lt;code&gt;merge.pgn&lt;/code&gt; — fast, memory-efficient, and correct.&lt;/p&gt;
&lt;p&gt;The result is &lt;a href=&quot;/projects/pgn-merger/&quot;&gt;PGN Merger&lt;/a&gt;, a .NET CLI tool built in F# that merges chess PGN files with streaming I/O and ~64 KB memory usage.&lt;/p&gt;
&lt;h2&gt;Why F# for a CLI Tool&lt;/h2&gt;
&lt;p&gt;F# is a functional-first language on .NET. Three features make it a strong fit for file-processing CLIs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discriminated unions&lt;/strong&gt; model success and failure at the type level:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FileResult =
    | Success of string * int64
    | Failure of string * string
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The compiler forces you to handle both cases. No nullable returns, no forgotten error checks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pattern matching&lt;/strong&gt; makes argument parsing readable and exhaustive:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;match args.[i] with
| &quot;--output&quot; when i + 1 &amp;lt; args.Length -&amp;gt;
    loop (i + 2) { opts with OutputPath = args.[i + 1] }
| &quot;--recursive&quot; -&amp;gt;
    loop (i + 1) { opts with Recursive = true }
| &quot;--help&quot; | &quot;-h&quot; | &quot;-?&quot; -&amp;gt;
    None
| arg when arg.StartsWith(&quot;--&quot;) -&amp;gt;
    printfn &quot;Error: Unknown option &apos;%s&apos;&quot; arg
    None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;First-class .NET interop&lt;/strong&gt; means &lt;code&gt;System.IO&lt;/code&gt;, &lt;code&gt;System.Diagnostics.Stopwatch&lt;/code&gt;, and high-performance streams are available directly. No FFI overhead. You write F#, you get native .NET performance.&lt;/p&gt;
&lt;h2&gt;Streaming I/O: Merging PGN Files Without Loading Them into Memory&lt;/h2&gt;
&lt;p&gt;The naive merge approach loads entire files into memory before writing. For a folder with 10 GB of PGN files, that&apos;s 10 GB in RAM before a single byte hits disk.&lt;/p&gt;
&lt;p&gt;PGN Merger never loads an entire file into memory. It streams through each file with 64 KB buffers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use inputStream = new FileStream(pgnFile, FileMode.Open, FileAccess.Read, FileShare.Read, 65536)
use reader = new StreamReader(inputStream, Encoding.UTF8, true, 65536)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The copy loop operates on a pre-allocated char array, allocated once per file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let streamFileToWriter (reader: StreamReader) (writer: StreamWriter) bufferSize =
    let buffer = Array.zeroCreate&amp;lt;char&amp;gt; bufferSize
    let mutable charsRead = 0
    while (charsRead &amp;lt;- reader.Read(buffer, 0, buffer.Length); charsRead &amp;gt; 0) do
        writer.Write(buffer, 0, charsRead)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No strings created during streaming. Memory footprint is ~64 KB per file regardless of input size. You could merge a 50 GB PGN archive on a machine with 512 MB of RAM.&lt;/p&gt;
&lt;p&gt;Both input and output streams use 64 KB buffers, which means writes are batched, the OS receives larger sequential requests, and the read-ahead cache can prefetch the next block. The access pattern is purely sequential: front-to-back reads, append-only writes. On a modern NVMe SSD, throughput is &lt;strong&gt;hundreds of MB/s&lt;/strong&gt;, bottlenecked by the storage device, not the code.&lt;/p&gt;
&lt;p&gt;Safety comes from F#&apos;s &lt;code&gt;use&lt;/code&gt; bindings (deterministic &lt;code&gt;Dispose()&lt;/code&gt; even on exceptions) and exhaustive exception handling per file. The tool returns non-zero exit codes on failure, making it script-friendly.&lt;/p&gt;
&lt;h2&gt;Discriminated Unions vs Exceptions for Error Handling&lt;/h2&gt;
&lt;p&gt;Most CLI tools use try-catch blocks and return -1 on any error. PGN Merger models every outcome explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type FileResult =
    | Success of string * int64       // filename, bytes written
    | Failure of string * string       // filename, error message
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each file processed produces a &lt;code&gt;FileResult&lt;/code&gt;. The merge loop collects these and reports partial success (exit code &lt;code&gt;2&lt;/code&gt;) when some files fail but others succeed. This is critical when you&apos;re merging 10,000 files — one corrupted file shouldn&apos;t kill the entire batch.&lt;/p&gt;
&lt;p&gt;The compiler enforces exhaustiveness. If you add a new case to &lt;code&gt;FileResult&lt;/code&gt;, every match expression breaks at compile time until you handle it. No silent failures.&lt;/p&gt;
&lt;h2&gt;Install and Try PGN Merger&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# Install from NuGet (requires .NET 10.0 SDK or later)
dotnet tool install --global PgnMerger

# Merge all PGN files in a folder
pgn-merger ./your_pgn_folder

# With verbose output and custom filename
pgn-merger ./your_pgn_folder --verbose --output database.pgn
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Browse the &lt;a href=&quot;https://www.nuget.org/packages/PgnMerger/0.2.0#readme-body-tab&quot;&gt;NuGet package page&lt;/a&gt; for version history, or check the &lt;a href=&quot;/projects/pgn-merger/&quot;&gt;project page&lt;/a&gt; for a feature overview, comparison table, and FAQ.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;More chess tooling&lt;/strong&gt;: I also built &lt;a href=&quot;/projects/chess/&quot;&gt;corentings/chess&lt;/a&gt;, a Go library for chess move generation, PGN encoding, and UCI interop. For tournament stories, read about my &lt;a href=&quot;/blog/karen-asrian-memorial-tournament/&quot;&gt;experience at the Karen Asrian Memorial&lt;/a&gt; in Armenia.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Related posts on performance and concurrency&lt;/strong&gt;: If you enjoyed the streaming I/O patterns here, you might like my posts on &lt;a href=&quot;/blog/semaphore-pattern-worker/&quot;&gt;worker pools in Go&lt;/a&gt; and &lt;a href=&quot;/blog/mergesort-parallel/&quot;&gt;parallel merge sort with goroutines&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>What Western UX Can Learn from Japanese Web Design</title><link>https://corentings.dev/blog/ux-japan-2/</link><guid isPermaLink="true">https://corentings.dev/blog/ux-japan-2/</guid><description>Japanese websites are often dismissed as cluttered. But their density reveals a different design logic: trust through context, comparison, and visible reassurance.</description><pubDate>Fri, 05 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What Western UX Can Learn from Japanese Web Design&lt;/h2&gt;
&lt;p&gt;Western web design has spent years removing things: navigation, sidebars, copy, options, hesitation. What remains is familiar: a hero image, a short promise, one button, three benefit cards, and maybe a testimonial.&lt;/p&gt;
&lt;p&gt;This can work. It can also become an ideology.&lt;/p&gt;
&lt;p&gt;Not every user is ready to click. Not every product can be understood through a slogan. Not every culture reads visual simplicity as trust. Japanese websites challenge one of contemporary UX&apos;s strongest assumptions: &lt;strong&gt;less information does not automatically mean more clarity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Western designers do not need to copy Japanese visual density. A Yahoo! Japan-style homepage will not improve a French SaaS landing page or an American portfolio site. The useful lesson is narrower and stronger:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What useful information have we removed in the name of simplicity?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2.png&quot; alt=&quot;A visual comparison of Japanese information-rich web design and Western minimalist UX, showing density as structured context rather than noise&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Japanese web density is useful when it gives context, comparison, and reassurance around the decision.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Context Is Part of Trust&lt;/h2&gt;
&lt;p&gt;Minimalist pages often assume the user needs a promise and a next step. Dense Japanese commercial pages often assume the user also needs the situation around that promise.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the price?&lt;/li&gt;
&lt;li&gt;What are the conditions?&lt;/li&gt;
&lt;li&gt;What are the alternatives?&lt;/li&gt;
&lt;li&gt;Is this campaign still active?&lt;/li&gt;
&lt;li&gt;What happens after clicking?&lt;/li&gt;
&lt;li&gt;Where is support?&lt;/li&gt;
&lt;li&gt;Is the company real?&lt;/li&gt;
&lt;li&gt;Can I compare before deciding?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In high-risk or high-comparison categories such as finance, healthcare, travel, electronics, education, insurance, government services, subscriptions, and B2B procurement, a beautiful sparse page can feel elegant but insufficient. It can make the user wonder what is being hidden.&lt;/p&gt;
&lt;p&gt;A sparse page says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Trust us. Click.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A reassurance-rich page says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here is the information you need to trust us.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Neither model is universally better. The trap is mistaking visual confidence for user confidence. A page can look calm while leaving too many practical questions unanswered.&lt;/p&gt;
&lt;p&gt;Visible care builds trust that polish alone cannot.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Problem Is Unmanaged Density&lt;/h2&gt;
&lt;p&gt;Dense design has costs. Japanese websites can overload the eye, bury priority, create accessibility problems, rely on image-based text, break on mobile, and reflect stakeholder politics more than user needs.&lt;/p&gt;
&lt;p&gt;Research on visual complexity and website search shows that complexity can slow people down. Even users familiar with dense pages are affected by weak hierarchy, poor grouping, and excessive competition between elements.&lt;/p&gt;
&lt;p&gt;The real divide is between &lt;strong&gt;accumulated density&lt;/strong&gt; and &lt;strong&gt;designed density&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Accumulated density happens when every department adds a banner, every campaign demands homepage space, every legal concern becomes a warning block, and every stakeholder protects a link.&lt;/p&gt;
&lt;p&gt;Designed density accepts that users need information, then organizes it into clear layers. It uses hierarchy, grouping, rhythm, disclosure, and strong labeling. It gives the user context without making every element shout.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Structured Density: Bento, Ma, and Service Counters&lt;/h2&gt;
&lt;p&gt;One Japanese-inspired pattern that travels well is the bento layout. A bento box can contain many things, but each thing has a place. Variety does not become chaos because the structure is visible.&lt;/p&gt;
&lt;p&gt;In interface design, a bento approach groups compact modules around user needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;main value proposition&lt;/li&gt;
&lt;li&gt;trust signals&lt;/li&gt;
&lt;li&gt;pricing or conditions&lt;/li&gt;
&lt;li&gt;comparison&lt;/li&gt;
&lt;li&gt;user proof&lt;/li&gt;
&lt;li&gt;support&lt;/li&gt;
&lt;li&gt;related actions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The page becomes a set of zones rather than a wall of competing elements.&lt;/p&gt;
&lt;p&gt;This connects to another useful Japanese concept: &lt;strong&gt;ma&lt;/strong&gt;, often translated as interval, pause, or meaningful space. Ma is the gap that lets elements make sense together.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-ma.png&quot; alt=&quot;A diagram explaining ma in interface design as meaningful interval between dense modules, decision points, and moments of reassurance&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ma is not empty space for decoration. It is the interval that helps dense information become readable.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In UI, ma can separate decision groups, calm a high-commitment action, distinguish promotional content from functional content, or create rhythm between dense modules.&lt;/p&gt;
&lt;p&gt;A dense page without ma becomes exhausting. A minimal page without context becomes vague. A strong interface needs both: information and interval.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;From Funnel to Service Counter&lt;/h2&gt;
&lt;p&gt;A Western landing page often behaves like a funnel. Its job is to move the user toward one action.&lt;/p&gt;
&lt;p&gt;A Japanese commercial homepage often behaves more like a service counter. Its job is to answer multiple kinds of users at once: new visitors, returning customers, campaign shoppers, comparison seekers, support users, loyalty members, and people who need reassurance before acting.&lt;/p&gt;
&lt;p&gt;From a conversion-funnel perspective, this looks inefficient. But not every page should be a pure funnel. Some pages need to orient. Some need to compare. Some need to reassure. Some need to support exploration before conversion.&lt;/p&gt;
&lt;p&gt;The interface should not force evaluating users into a checkout mindset too early.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Western Designers Can Borrow&lt;/h2&gt;
&lt;p&gt;Japanese web design is not a style to copy directly. It is a set of questions worth importing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What does the user need to know before they can trust this action?&lt;/li&gt;
&lt;li&gt;Are we hiding information users need for comparison?&lt;/li&gt;
&lt;li&gt;Do we support exploration as well as conversion?&lt;/li&gt;
&lt;li&gt;Which details reduce anxiety, and which details merely compete for attention?&lt;/li&gt;
&lt;li&gt;Can density adapt to first-time users, returning users, experts, mobile users, and cautious buyers?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Put anxiety-reducing details close to the moment of decision: secure payment, cancellation terms, delivery estimate, support availability, official status, setup time, compatibility, or return policy.&lt;/p&gt;
&lt;p&gt;Do not borrow banner competition, image-based text, weak hierarchy, purposeless duplicate links, decorative mascots that do not help the task, or the habit of making every stakeholder equally visible.&lt;/p&gt;
&lt;p&gt;A useful redesign asks one question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What is the user&apos;s uncertainty, and what interface structure reduces it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Toward Trustful Minimalism&lt;/h2&gt;
&lt;p&gt;A better term than maximalism or pure minimalism is &lt;strong&gt;trustful minimalism&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Trustful minimalism keeps the visual discipline of modern UX but restores the information users need to feel confident. It still defines a primary action. It still uses hierarchy and space. But it does not rely on brand mood alone.&lt;/p&gt;
&lt;p&gt;For a Western product page, landing page, or service page, the framework is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Define the primary action.&lt;/li&gt;
&lt;li&gt;Identify the top anxieties blocking that action: price, setup time, cancellation, delivery, compatibility, support, security, legitimacy, return policy.&lt;/li&gt;
&lt;li&gt;Place short reassurance cues near the action.&lt;/li&gt;
&lt;li&gt;Group deeper details into expandable sections, comparison modules, or bento-style cards.&lt;/li&gt;
&lt;li&gt;Separate promotional content from decision-critical content.&lt;/li&gt;
&lt;li&gt;Test perceived trust, completeness, and confidence, not only conversion speed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A redesign can make a page look cleaner while making users less certain. If the only metric is visual preference, the team may remove exactly the elements that made the page trustworthy.&lt;/p&gt;
&lt;p&gt;Good design means enough: enough context to understand, enough hierarchy to choose, enough reassurance to trust, enough space to think, and enough restraint to avoid overload.&lt;/p&gt;
&lt;p&gt;The useful lesson from Japanese information-rich design is not more decoration. It is a better design question: what context does the user need, and what can we remove without weakening trust?&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;References and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Richard E. Nisbett and Takahiko Masuda, &quot;Culture and Point of View&quot;&lt;/li&gt;
&lt;li&gt;Takahiko Masuda and Richard E. Nisbett, &quot;Attending Holistically Versus Analytically&quot;&lt;/li&gt;
&lt;li&gt;Nordhoff, August, Oliveira, and Reinecke, &quot;A Case for Design Localization&quot;&lt;/li&gt;
&lt;li&gt;Baughan et al., studies on visual complexity, website search, and cross-cultural attention&lt;/li&gt;
&lt;li&gt;Dianne Cyr and Haizley Trevor-Smith, &quot;Localization of Web Design&quot;&lt;/li&gt;
&lt;li&gt;Elizabeth Würtz, &quot;Intercultural Communication on Web Sites&quot;&lt;/li&gt;
&lt;li&gt;W3C, &quot;Requirements for Japanese Text Layout&quot;&lt;/li&gt;
&lt;li&gt;Japan House Los Angeles, &quot;A Perspective on the Japanese Concept of Ma&quot;&lt;/li&gt;
&lt;li&gt;Stanford Encyclopedia of Philosophy, &quot;Japanese Aesthetics&quot;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Why Japanese Websites Look Overloaded: Density, Tokyo, and Trust</title><link>https://corentings.dev/blog/ux-japan-1/</link><guid isPermaLink="true">https://corentings.dev/blog/ux-japan-1/</guid><description>Why Japanese websites look overloaded, how Tokyo&apos;s dense visual environment helps explain the pattern, and why &apos;clutter&apos; is often the wrong UX question.</description><pubDate>Sun, 31 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why Japanese Websites Look Overloaded: Density, Tokyo, and Trust&lt;/h2&gt;
&lt;p&gt;Open Yahoo! Japan after browsing a Western startup website and the contrast is immediate. The Western page gives you a hero image, a sentence, and a button. The Japanese page gives you news, weather, shopping, finance, auctions, login, campaigns, rankings, emergency notices, and many routes elsewhere.&lt;/p&gt;
&lt;p&gt;To a Western designer, this often looks like clutter. The better UX question asks: &lt;strong&gt;what job is this density doing?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many Japanese domestic websites use high-density interfaces to show context, reassurance, comparison options, promotions, navigation, and trust cues at once. The result can be messy, but it is not always meaningless.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-1.jpg&quot; alt=&quot;A typical Japanese domestic portal page showing dense navigation, campaign banners, multiple content modules, and trust cues visible on a single screen&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Japanese portal homepage showing high-density UI: news, weather, campaigns, rankings, and navigation modules are all visible at once.&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Density Is Not One Thing&lt;/h2&gt;
&lt;p&gt;Calling everything &quot;clutter&quot; hides the differences that matter.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Always bad?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Information density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shows facts, specs, labels, notices, comparisons&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Navigational density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Gives many routes, menus, categories, breadcrumbs&lt;/td&gt;
&lt;td&gt;Sometimes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Promotional density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shows coupons, rankings, campaigns, limited-time offers&lt;/td&gt;
&lt;td&gt;Depends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trust density&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Shows security, support, returns, company info, official notices&lt;/td&gt;
&lt;td&gt;Often useful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Technical bloat&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adds heavy JS, slow assets, tracking, excessive DOM&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Only technical bloat is inherently bad. The rest can serve a function, even if poorly executed. A page with visible delivery conditions is not the same problem as a page slowed down by unoptimized scripts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-4.png&quot; alt=&quot;Diagram of six density types: information, visual, navigational, promotional, trust, and technical bloat&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Separating density types lets us diagnose what each part of the page is doing, rather than collapsing everything into &quot;clutter.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Websites as Service Counters&lt;/h2&gt;
&lt;p&gt;Western landing pages often behave like posters: one dominant message, one focal object, one call to action. Many Japanese portals, service pages, and retail sites behave more like maps, directories, counters, or flyers. They assume users arrive with questions, comparisons, and multiple possible next steps.&lt;/p&gt;
&lt;p&gt;A Japanese homepage often answers several questions upfront:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is new?&lt;/li&gt;
&lt;li&gt;What campaign is active?&lt;/li&gt;
&lt;li&gt;Where do I log in?&lt;/li&gt;
&lt;li&gt;Where is support?&lt;/li&gt;
&lt;li&gt;What are the rankings?&lt;/li&gt;
&lt;li&gt;What are the conditions?&lt;/li&gt;
&lt;li&gt;Can I compare?&lt;/li&gt;
&lt;li&gt;Is this official?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this model, the page is part storefront, part catalogue, part customer-service desk, part seasonal promotion board. The density supports multiple user intents at once.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-1-3.jpg&quot; alt=&quot;Japanese homepage annotated with service-counter zones: information, campaigns, navigation, trust cues, and announcements&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Japanese homepage annotated as a service counter: each cluster answers a different user question before it needs to be asked.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This does not make every dense page good. A service counter can still be chaotic. The point is that the density may be trying to solve a different design problem from a minimalist landing page.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Tokyo Helps Explain the Pattern&lt;/h2&gt;
&lt;p&gt;Japanese web density makes more sense beside the dense physical and media environments many users navigate every day.&lt;/p&gt;
&lt;p&gt;Walk through Shinjuku Station at rush hour and information is part of the architecture: platform numbers, exit codes, train lines, arrows, warnings, shop signs, campaign posters, ticket machines, and convenience-store shelves. The first impression is density. The second is that people know how to move through it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-1.jpg&quot; alt=&quot;Layered signage and commercial density on a Tokyo street at night&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Japanese urban density is often vertical, layered, and sign-rich. A domestic portal page can feel less strange when read through this spatial logic.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Tokyo did not cause Japanese web design. But Tokyo, Akihabara, department-store floor guides, konbini shelves, ticket machines, manga pages, chirashi flyers, and public notice boards all train similar habits: scan, filter, compare, follow labels, and enter from more than one point.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./images/jap-ux-2-2.jpg&quot; alt=&quot;Japanese train station wayfinding signage with color-coded lines and multiple information layers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Dense is not random. Japanese stations compress routes, exits, platforms, warnings, and services into navigable systems.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The useful analogy is that both city and website can operate as navigable information environments.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What Research Suggests&lt;/h2&gt;
&lt;p&gt;The science is suggestive, not conclusive. It does not prove that Japanese users prefer dense websites. But it does show that attention is not culturally neutral.&lt;/p&gt;
&lt;p&gt;Masuda and Nisbett&apos;s work on analytic versus holistic attention found that Western participants tended to focus more on focal objects, while Japanese participants reported more contextual and relational information. Their 2001 underwater-scene study was not about websites. It suggests one design hypothesis: surrounding interface cues may carry more meaning for users trained in context-rich environments.&lt;/p&gt;
&lt;p&gt;Miyamoto, Nisbett, and Masuda later compared Japanese and American physical environments and found that Japanese scenes tended to contain more elements, ambiguity, and overlapping relationships. The built environment is not just scenery. It can train perception.&lt;/p&gt;
&lt;p&gt;Cross-cultural web-design research also finds measurable visual differences. Nordhoff, August, Oliveira, and Reinecke analyzed &lt;strong&gt;80,901 website designs across 44 countries&lt;/strong&gt; and found that Japan, China, and South Korea clustered toward higher visual complexity and text density.&lt;/p&gt;
&lt;p&gt;But familiarity is not efficiency. Baughan and colleagues studied &lt;strong&gt;84 U.S. American and 65 Japanese participants&lt;/strong&gt; searching website screenshots of varying complexity. Japanese participants took longer overall, and complexity hurt search efficiency. Dense interfaces may be locally familiar and commercially meaningful, but they still need hierarchy.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Density Still Needs Hierarchy&lt;/h2&gt;
&lt;p&gt;Japanese text composition also changes what compact information can look like. The W3C&apos;s &lt;em&gt;Requirements for Japanese Text Layout&lt;/em&gt; documents mixed writing systems, vertical and horizontal composition, line-breaking rules, ruby annotations, emphasis marks, and spacing conventions.&lt;/p&gt;
&lt;p&gt;Japanese script does not cause clutter. But kanji, kana, Latin characters, numerals, and compact labels create different possibilities for dense display than English or French interfaces.&lt;/p&gt;
&lt;p&gt;The useful lesson is &lt;strong&gt;structured density&lt;/strong&gt;: context-rich design that does not sacrifice usability. Useful density supports comparison, reassurance, and navigation. Costly density hides priority, forces exhaustive search, and makes every element compete.&lt;/p&gt;
&lt;p&gt;Some Japanese websites are cluttered. Visual hierarchy can collapse. Promotional banners can multiply until they obscure the thing they promote. Technical bloat can make a page slow and inaccessible.&lt;/p&gt;
&lt;p&gt;What looks like clutter from one design ideology can be context, reassurance, comparison, and transparency from another. A Western landing page says: &quot;here is the action.&quot; A Japanese high-density page often says: &quot;here is the situation.&quot;&lt;/p&gt;
&lt;p&gt;The UX test is simple: what happens if you remove an element? Does the user lose information they needed, confidence they relied on, or a route they expected? If yes, it was not only clutter. It was structure.&lt;/p&gt;
&lt;p&gt;Instead of asking why Japanese websites are overloaded, ask what the overload is doing.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Sources and Further Reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Baughan, A., Oliveira, N., August, T., Yamashita, N., &amp;amp; Reinecke, K. (2021). &quot;Do Cross-Cultural Differences in Visual Attention Patterns Affect Search Efficiency on Websites?&quot; &lt;em&gt;Proceedings of the 2021 CHI Conference on Human Factors in Computing Systems&lt;/em&gt;. &lt;a href=&quot;https://naomi-yamashita.net/wp-content/uploads/2021/06/2021jp-02.pdf&quot;&gt;PDF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cyr, D., &amp;amp; Trevor-Smith, H. &quot;Localization of Web Design: An Empirical Comparison of German, Japanese, and United States Web Site Characteristics.&quot; &lt;em&gt;Journal of the American Society for Information Science and Technology&lt;/em&gt;, 55(13), 1199-1208, 2004.&lt;/li&gt;
&lt;li&gt;Masuda, T., &amp;amp; Nisbett, R. E. (2001). &quot;Attending Holistically Versus Analytically: Comparing the Context Sensitivity of Japanese and Americans.&quot; &lt;em&gt;Journal of Personality and Social Psychology&lt;/em&gt;, 81(5), 922-934.&lt;/li&gt;
&lt;li&gt;Miyamoto, Y., Nisbett, R. E., &amp;amp; Masuda, T. &quot;Culture and the Physical Environment: Holistic Versus Analytic Perceptual Affordances.&quot; &lt;em&gt;Psychological Science&lt;/em&gt;, 17(2), 113-119, 2006.&lt;/li&gt;
&lt;li&gt;Nisbett, R. E., &amp;amp; Masuda, T. (2003). &quot;Culture and Point of View.&quot; &lt;em&gt;Proceedings of the National Academy of Sciences&lt;/em&gt;, 100(19), 11163-11170.&lt;/li&gt;
&lt;li&gt;Nordhoff, M., August, T., Oliveira, N. A., &amp;amp; Reinecke, K. (2018). &quot;A Case for Design Localization: Diversity of Website Aesthetics in 44 Countries.&quot; &lt;em&gt;Proceedings of the 2018 CHI Conference on Human Factors in Computing Systems&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;W3C. (2020). &quot;Requirements for Japanese Text Layout.&quot; &lt;a href=&quot;https://www.w3.org/TR/jlreq/&quot;&gt;JLREQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Würtz, E. &quot;Intercultural Communication on Web Sites: A Cross-Cultural Analysis of Web Sites from High-Context Cultures and Low-Context Cultures.&quot; &lt;em&gt;Journal of Computer-Mediated Communication&lt;/em&gt;, 11(1), 2005.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor</title><link>https://corentings.dev/blog/tdd-permission-slip/</link><guid isPermaLink="true">https://corentings.dev/blog/tdd-permission-slip/</guid><description>Learn why test-driven development is really about permission to refactor, not catching bugs. With TypeScript examples, Result&lt;T, E&gt; patterns, and behavior-based testing from 3 years in production.</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/h2&gt;
&lt;h2&gt;The Moment It Clicked&lt;/h2&gt;
&lt;p&gt;Three years ago, I had a crisis of confidence with test-driven development.&lt;/p&gt;
&lt;p&gt;I&apos;d built a user registration system with 80% test coverage, all green, CI passing. Then I decided to refactor the email validation logic: just move it to a separate module. A tiny change.&lt;/p&gt;
&lt;p&gt;Thirty minutes later, everything was broken.&lt;/p&gt;
&lt;p&gt;My tests tested &lt;strong&gt;how&lt;/strong&gt; the code worked, not &lt;strong&gt;what&lt;/strong&gt; it did. The moment the structure changed, they failed. I spent more time fixing tests than doing the refactoring itself.&lt;/p&gt;
&lt;p&gt;I&apos;ll show you what I was doing wrong. It might look familiar.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Trap: Testing Structure&lt;/h2&gt;
&lt;p&gt;Here&apos;s the TypeScript code I had, a &lt;code&gt;User&lt;/code&gt; class with inline validation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	email: string;
	age: number;

	constructor(email: string, age: number) {
		if (!email.includes(&quot;@&quot;)) {
			throw new Error(&quot;Invalid email&quot;);
		}
		if (age &amp;lt; 18) {
			throw new Error(&quot;Must be at least 18 years old&quot;);
		}
		this.email = email;
		this.age = age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the test:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe(&quot;User&quot;, () =&amp;gt; {
	it(&quot;should create a user&quot;, () =&amp;gt; {
		const user = new User(&quot;alice@example.com&quot;, 25);
		expect(user.email).toBe(&quot;alice@example.com&quot;);
		expect(user.age).toBe(25);
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What am I testing? Structure. I&apos;m reaching into the object and checking its properties directly.&lt;/p&gt;
&lt;p&gt;Now watch what happens when I hide the internals:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	private email: string;
	private age: number;

	constructor(email: string, age: number) {
		if (!email.includes(&quot;@&quot;)) {
			throw new Error(&quot;Invalid email&quot;);
		}
		if (age &amp;lt; 18) {
			throw new Error(&quot;Must be at least 18 years old&quot;);
		}
		this.email = email;
		this.age = age;
	}

	get email(): string {
		return this.email;
	}

	get age(): number {
		return this.age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The test breaks. The behavior hasn&apos;t changed (the same user is created with the same rules), but I changed the structure.&lt;/p&gt;
&lt;p&gt;I&apos;m now rewriting tests for a refactoring that shouldn&apos;t require any test changes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Fix: Test Behavior&lt;/h2&gt;
&lt;p&gt;The solution: test what the code &lt;strong&gt;does&lt;/strong&gt;, not how it&apos;s &lt;strong&gt;organized&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But first, I need to fix another problem. Throwing exceptions makes testing awkward: you need &lt;code&gt;expect(() =&amp;gt; fn()).toThrow()&lt;/code&gt;, which hides the error behind a callback. The fix is treating errors as &lt;strong&gt;first-class values&lt;/strong&gt; instead of hidden control flow.&lt;/p&gt;
&lt;p&gt;Here&apos;s the pattern that made this click, the &lt;code&gt;Result&lt;/code&gt; type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Result&amp;lt;T, E&amp;gt; =
	| { readonly ok: true; readonly value: T }
	| { readonly ok: false; readonly error: E };

function ok&amp;lt;T&amp;gt;(value: T): Result&amp;lt;T, never&amp;gt; {
	return { ok: true, value };
}

function err&amp;lt;E&amp;gt;(error: E): Result&amp;lt;never, E&amp;gt; {
	return { ok: false, error };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of throwing, functions return a &lt;code&gt;Result&lt;/code&gt; that&apos;s either a success or a failure. Both paths are explicit. No try/catch. Errors are data you can inspect, test, and chain. (In production code, you&apos;d use a library like &lt;a href=&quot;https://github.com/supermacro/neverthrow&quot;&gt;neverthrow&lt;/a&gt; which adds &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;flatMap&lt;/code&gt;, and &lt;code&gt;match&lt;/code&gt; for composing results.)&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;Result&lt;/code&gt; in place, I can rewrite the &lt;code&gt;User&lt;/code&gt; class with a static factory method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class UserCreationError {
	constructor(
		public readonly message: string,
		public readonly field: string
	) {}
}

class User {
	private constructor(
		private readonly _email: string,
		private readonly _age: number
	) {}

	static create(email: string, age: number): Result&amp;lt;User, UserCreationError&amp;gt; {
		if (!email.includes(&quot;@&quot;)) {
			return err(new UserCreationError(&quot;Invalid email format&quot;, &quot;email&quot;));
		}
		if (age &amp;lt; 18) {
			return err(new UserCreationError(&quot;Must be at least 18 years old&quot;, &quot;age&quot;));
		}
		return ok(new User(email, age));
	}

	get email(): string {
		return this._email;
	}

	get age(): number {
		return this._age;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the tests verify &lt;strong&gt;behavior&lt;/strong&gt;, the observable contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe(&quot;User Registration&quot;, () =&amp;gt; {
	it(&quot;should create a user with valid email and age&quot;, () =&amp;gt; {
		const result = User.create(&quot;alice@example.com&quot;, 25);

		expect(result.ok).toBe(true);
		if (result.ok) {
			expect(result.value.email).toBe(&quot;alice@example.com&quot;);
			expect(result.value.age).toBe(25);
		}
	});

	it(&quot;should reject invalid emails&quot;, () =&amp;gt; {
		const result = User.create(&quot;not-an-email&quot;, 25);

		expect(result.ok).toBe(false);
		if (!result.ok) {
			expect(result.error.field).toBe(&quot;email&quot;);
			expect(result.error.message).toMatch(/email/i);
		}
	});

	it(&quot;should reject users under 18&quot;, () =&amp;gt; {
		const result = User.create(&quot;alice@example.com&quot;, 17);

		expect(result.ok).toBe(false);
		if (!result.ok) {
			expect(result.error.message).toMatch(/18/);
		}
	});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When I refactor the internals now (make &lt;code&gt;_email&lt;/code&gt; private, change the data structure, extract a class), these tests keep passing. They test the contract: valid inputs succeed, invalid inputs fail with the right error. The internal structure is irrelevant.&lt;/p&gt;
&lt;p&gt;This is the red-green-refactor cycle that defines test-driven development: write a failing test (red), make it pass with the simplest code (green), then improve the design while keeping tests green (refactor). The tests give you permission to refactor aggressively because they catch real problems, not structural changes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Going Deeper: Value Objects&lt;/h2&gt;
&lt;p&gt;Once I started testing behavior, I noticed something. My validation logic was scattered. The same email check appeared in &lt;code&gt;registerUser&lt;/code&gt;, &lt;code&gt;changeEmail&lt;/code&gt;, and &lt;code&gt;resetPassword&lt;/code&gt;. Three places, same rule, same potential for bugs.&lt;/p&gt;
&lt;p&gt;The fix is a &lt;strong&gt;Value Object&lt;/strong&gt;: a small, self-validating type that represents a single concept. If you have an &lt;code&gt;Email&lt;/code&gt; instance, it&apos;s valid by construction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class InvalidEmailError {
	readonly message = &quot;Invalid email format&quot;;
}

class Email {
	private constructor(private readonly _value: string) {}

	static create(raw: string): Result&amp;lt;Email, InvalidEmailError&amp;gt; {
		const value = raw.trim().toLowerCase();
		if (!value.includes(&quot;@&quot;)) {
			return err(new InvalidEmailError());
		}
		return ok(new Email(value));
	}

	get value(): string {
		return this._value;
	}

	equals(other: Email): boolean {
		return this._value === other._value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the normalization (&lt;code&gt;trim().toLowerCase()&lt;/code&gt;) lives inside the Value Object. Every &lt;code&gt;Email&lt;/code&gt; instance is guaranteed lowercase and trimmed. The validation rule exists in one place. If the rule changes, you change one class.&lt;/p&gt;
&lt;p&gt;Similarly for &lt;code&gt;Age&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class InvalidAgeError {
	readonly message = &quot;Must be at least 18 years old&quot;;
}

class Age {
	private constructor(private readonly _value: number) {}

	static create(value: number): Result&amp;lt;Age, InvalidAgeError&amp;gt; {
		if (!Number.isInteger(value) || value &amp;lt; 18) {
			return err(new InvalidAgeError());
		}
		return ok(new Age(value));
	}

	get value(): number {
		return this._value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;User&lt;/code&gt; class now composes these Value Objects instead of accepting raw primitives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class User {
	private constructor(
		private readonly _email: Email,
		private readonly _age: Age
	) {}

	static create(email: string, age: number): Result&amp;lt;User, UserCreationError&amp;gt; {
		const emailResult = Email.create(email);
		if (!emailResult.ok)
			return err(new UserCreationError(emailResult.error.message, &quot;email&quot;));

		const ageResult = Age.create(age);
		if (!ageResult.ok)
			return err(new UserCreationError(ageResult.error.message, &quot;age&quot;));

		return ok(new User(emailResult.value, ageResult.value));
	}

	get email(): string {
		return this._email.value;
	}

	get age(): number {
		return this._age.value;
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same tests from before still pass. They test &lt;code&gt;User.create()&lt;/code&gt; returning success or failure, which hasn&apos;t changed. The internal composition with Value Objects is invisible to the tests.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Payment System&lt;/h2&gt;
&lt;p&gt;Last year, I refactored a production payment processing module: about 2,400 lines of conditional logic scattered across &lt;code&gt;processPayment&lt;/code&gt;, &lt;code&gt;validatePayment&lt;/code&gt;, and &lt;code&gt;handleRefund&lt;/code&gt;. Validation rules were duplicated in all three functions.&lt;/p&gt;
&lt;p&gt;The old tests looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it(&quot;processes payment&quot;, () =&amp;gt; {
	const processor = new PaymentProcessor(mockGateway, mockDB);
	processor.validateAmount(100);
	processor.validateCurrency(&quot;USD&quot;);
	const result = processor.process({ amount: 100, currency: &quot;USD&quot; });
	expect(result.status).toBe(&quot;success&quot;);
	expect(mockGateway.charge).toHaveBeenCalledTimes(1);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those tests called internal methods directly. When I extracted validation into a &lt;code&gt;Payment&lt;/code&gt; Value Object, every test broke because &lt;code&gt;validateAmount&lt;/code&gt; and &lt;code&gt;validateCurrency&lt;/code&gt; no longer existed as separate methods.&lt;/p&gt;
&lt;p&gt;I rewrote the tests to focus on the contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;it(&quot;accepts valid payments&quot;, () =&amp;gt; {
	const result = Payment.create({ amount: 100, currency: &quot;USD&quot; });
	expect(result.ok).toBe(true);
});

it(&quot;rejects negative amounts&quot;, () =&amp;gt; {
	const result = Payment.create({ amount: -50, currency: &quot;USD&quot; });
	expect(result.ok).toBe(false);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I refactored aggressively. Extracted &lt;code&gt;Currency&lt;/code&gt; as its own Value Object. Moved validation into the &lt;code&gt;Payment&lt;/code&gt; factory. Deleted 800 lines of duplicated logic. Tests never broke once, because they tested what happened, not how.&lt;/p&gt;
&lt;p&gt;The whole refactoring took a day. With the old tests, it would have taken a week: three days of refactoring, two days of fixing tests.&lt;/p&gt;
&lt;p&gt;The same idea shows up in concurrency code too. When you refactor a &lt;a href=&quot;/blog/go-pattern-pipeline/&quot;&gt;pipeline&lt;/a&gt; or a &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;worker pool&lt;/a&gt;, the contract stays the same while the internals change. The tests that survive are the ones that verify behavior, not goroutine counts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The &quot;TDD Is Slow&quot; Myth&lt;/h2&gt;
&lt;p&gt;I used to think test-driven development slowed me down. Studies tell a more nuanced story: TDD has a modest upfront cost of 10–30% more development time, but reduces defects by 40–90% (Nagappan et al., Microsoft/IBM, 2008). George &amp;amp; Williams (2003) found TDD pairs took 16% more time but passed 18% more black-box tests.&lt;/p&gt;
&lt;p&gt;The upfront cost is real. The payoff is also real. And it compounds: every safe refactoring makes the next one easier.&lt;/p&gt;
&lt;p&gt;When I couldn&apos;t refactor safely, I coded defensively. I avoided changes. I left bad code in place because changing it was risky. Refactoring without tests is what&apos;s actually slow. It compounds with every month.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;When TDD Doesn&apos;t Help&lt;/h2&gt;
&lt;p&gt;Test-driven development isn&apos;t always the right tool. Spikes and prototypes (code you&apos;ll throw away) don&apos;t need tests. Exploratory work where you don&apos;t know the shape of the solution yet. UI code where the behavior is visual, not logical. One-off scripts. Code with genuinely unstable requirements that change every sprint.&lt;/p&gt;
&lt;p&gt;TDD pays off when you&apos;re building something you&apos;ll maintain and refactor over time. If the code won&apos;t survive the week, write the minimum.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Monday Morning&lt;/h2&gt;
&lt;p&gt;Take one function you wrote recently. Rewrite its tests to verify behavior (what it does) instead of implementation (how it&apos;s organized).&lt;/p&gt;
&lt;p&gt;Watch what happens to your code.&lt;/p&gt;
&lt;p&gt;Three years ago, I was afraid to refactor. Now I do it aggressively, because my tests give me permission. Test-driven development gave me a permission slip I didn&apos;t know I needed.&lt;/p&gt;
&lt;p&gt;Next time, I&apos;ll show you how to build Value Objects that make invalid states impossible, and how testing them first makes the design emerge naturally.&lt;/p&gt;
&lt;p&gt;If you found this useful, the next article in the series shows how to build Value Objects that make invalid states impossible. It comes out in a couple of weeks. You can subscribe via &lt;a href=&quot;/rss.xml&quot;&gt;RSS&lt;/a&gt; or &lt;a href=&quot;/atom.xml&quot;&gt;Atom&lt;/a&gt; to catch it when it lands.&lt;/p&gt;
&lt;p&gt;— Corentin&lt;/p&gt;
</content:encoded></item><item><title>Docker 29 Broke Traefik — Here&apos;s the Fix (and Why It Happened)</title><link>https://corentings.dev/blog/docker-29-traefik-fix/</link><guid isPermaLink="true">https://corentings.dev/blog/docker-29-traefik-fix/</guid><description>Docker Engine v29 raised the minimum API version to 1.44, breaking Traefik and a dozen other tools. Here&apos;s the daemon.json fix that takes 30 seconds, and the real solution.</description><pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Docker 29 Broke Traefik — Here&apos;s the Fix (and Why It Happened)&lt;/h2&gt;
&lt;h2&gt;The &quot;Everything Is Down&quot; Moment&lt;/h2&gt;
&lt;p&gt;You ran &lt;code&gt;apt upgrade&lt;/code&gt; on a Sunday evening on your Linux server. Seemed harmless. Docker updated to v29.0.0. You rebooted because why not.&lt;/p&gt;
&lt;p&gt;Now every service behind your Traefik reverse proxy returns a 404. Or a 502. Or just… nothing. Your monitoring is screaming. Your personal projects are unreachable. Your CI pipeline is stuck. And if you&apos;re running Coolify, Dokploy, or any platform that bundles Traefik, you can&apos;t even access the management UI to fix it.&lt;/p&gt;
&lt;p&gt;You check the Traefik logs and see this, repeating every few seconds:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ERR Failed to retrieve information of the docker client and server host
  error=&quot;Error response from daemon: client version 1.24 is too old.
  Minimum supported API version is 1.44, please upgrade your client
  to a newer version&quot; providerName=docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Your first thought: &quot;I didn&apos;t change anything in Traefik.&quot;&lt;/p&gt;
&lt;p&gt;You&apos;re right. You didn&apos;t. Docker changed something under you.&lt;/p&gt;
&lt;p&gt;Let me save you the debugging. Here&apos;s the fix.&lt;/p&gt;
&lt;h2&gt;The 30-Second Fix&lt;/h2&gt;
&lt;p&gt;This applies to &lt;strong&gt;Linux hosts running Docker Engine directly&lt;/strong&gt;. If you&apos;re using Docker Desktop (macOS or Windows), you&apos;re not affected — updates are bundled automatically.&lt;/p&gt;
&lt;p&gt;If your services are down right now and you need them back immediately, do this:&lt;/p&gt;
&lt;p&gt;Edit (or create) &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /etc/docker/daemon.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;min-api-version&quot;: &quot;1.24&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you already have content in &lt;code&gt;daemon.json&lt;/code&gt; (storage driver, log driver, etc.), merge the key into the existing object. Don&apos;t overwrite your whole config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;log-driver&quot;: &quot;json-file&quot;,
	&quot;log-opts&quot;: {
		&quot;max-size&quot;: &quot;10m&quot;,
		&quot;max-file&quot;: &quot;3&quot;
	},
	&quot;min-api-version&quot;: &quot;1.24&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most common mistake is a missing or trailing comma. JSON is strict about this.&lt;/p&gt;
&lt;p&gt;Then restart Docker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Restarting Docker stops all running containers. If your containers have a restart policy (&lt;code&gt;restart: unless-stopped&lt;/code&gt; or &lt;code&gt;restart: always&lt;/code&gt;), they&apos;ll come back on their own. If not, you&apos;ll need to start them manually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Docker Swarm:&lt;/strong&gt; This fix works the same on Swarm nodes. Apply it to all managers and workers. Swarm services will reschedule automatically after the daemon restarts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rootless Docker?&lt;/strong&gt; The config file lives at &lt;code&gt;~/.config/docker/daemon.json&lt;/code&gt; and you restart with &lt;code&gt;systemctl --user restart docker&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That&apos;s it. Your services should come back within seconds. Traefik will reconnect to the Docker daemon, discover your containers, and routes will light up again.&lt;/p&gt;
&lt;h3&gt;Verifying it worked&lt;/h3&gt;
&lt;p&gt;Check Traefik&apos;s logs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker logs traefik --tail 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should &lt;strong&gt;no longer&lt;/strong&gt; see the &lt;code&gt;client version 1.24 is too old&lt;/code&gt; error. If you see &lt;code&gt;Configuration received from provider docker&lt;/code&gt;, that&apos;s a positive confirmation (this message only appears at INFO log level, which may not be your default).&lt;/p&gt;
&lt;p&gt;The strongest verification is the API version check below.&lt;/p&gt;
&lt;p&gt;Confirm your Docker API versions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker version --format &apos;{{.Server.MinAPIVersion}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should now return &lt;code&gt;1.24&lt;/code&gt; instead of &lt;code&gt;1.44&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why this works:&lt;/strong&gt; The &lt;code&gt;min-api-version&lt;/code&gt; setting overrides Docker&apos;s built-in floor. It tells the daemon to accept connections from clients using API version 1.24 and above, restoring the backward compatibility that Docker 29 removed. This config change keeps Docker 29 fully operational while re-enabling communication with older API clients. You&apos;re still running Docker 29 with all its features — you&apos;ve just told it to accept a wider range of client versions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is this safe?&lt;/strong&gt; For now, yes. This setting only changes how Docker negotiates API versions with clients. The API version is a negotiation protocol that controls feature detection between client and server. It doesn&apos;t touch encryption or authentication. The risk is that older API clients might miss newer fields or features, but Traefik only needs to read container labels and inspect networks. It doesn&apos;t need API 1.44 for that.&lt;/p&gt;
&lt;h2&gt;The Real Fix: Update Traefik&lt;/h2&gt;
&lt;p&gt;The daemon.json workaround is a bandage. The actual fix is updating Traefik to &lt;strong&gt;v3.6.1 or later&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Traefik v3.6.1, released shortly after this incident, added automatic Docker API version negotiation. Instead of hardcoded v1.24, Traefik now queries the daemon for its supported version range and picks the highest mutually compatible version. This is how the Docker SDK is supposed to work.&lt;/p&gt;
&lt;p&gt;If you manage Traefik directly (docker-compose, Dockerfile, etc.), change your image tag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;services:
  traefik:
    image: traefik:v3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or pull the latest:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull traefik:v3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then restart:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose up -d traefik
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re on the older standalone &lt;code&gt;docker-compose&lt;/code&gt;, the command is &lt;code&gt;docker-compose up -d traefik&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once Traefik is on 3.6.1+, you can remove the &lt;code&gt;&quot;min-api-version&quot;&lt;/code&gt; workaround from &lt;code&gt;daemon.json&lt;/code&gt; and restart Docker again. Everything will keep working because Traefik now negotiates the API version correctly on its own.&lt;/p&gt;
&lt;h3&gt;If you can&apos;t update Traefik directly&lt;/h3&gt;
&lt;p&gt;If you&apos;re running a platform that bundles Traefik (Coolify, Dokploy, Appwrite, CapRover), you might not control the Traefik image version. In that case:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check if your platform has an update.&lt;/strong&gt; Most of them shipped patches within days.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use the daemon.json workaround&lt;/strong&gt; until the platform updates its bundled Traefik.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check the platform&apos;s GitHub issues.&lt;/strong&gt; This broke so many deployments that every affected platform has a thread about it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What Actually Happened&lt;/h2&gt;
&lt;p&gt;Let me explain the technical chain of events, because understanding it helps you prevent similar breakages.&lt;/p&gt;
&lt;h3&gt;Docker&apos;s API version floor&lt;/h3&gt;
&lt;p&gt;Docker Engine exposes a versioned API. Clients (CLI, SDK, third-party tools) negotiate which version to use during the initial handshake. Before Docker 29, the daemon supported API versions going all the way back to v1.24, which was introduced with Docker 1.12 in 2016. Nearly a decade of backward compatibility.&lt;/p&gt;
&lt;p&gt;With Docker Engine v29.0.0 (released November 10, 2025), Docker raised the minimum supported API version to &lt;strong&gt;v1.44&lt;/strong&gt;. API v1.44 corresponds to Docker Engine v25, released in late 2023.&lt;/p&gt;
&lt;p&gt;This was deliberate and documented. Docker&apos;s deprecation policy states that API versions are supported for a limited number of major releases. API 1.24 had been deprecated for years. Docker 29 finally enforced the deprecation.&lt;/p&gt;
&lt;p&gt;The result: any tool compiled against an older Docker SDK that defaults to API v1.24 as its negotiation floor gets rejected immediately. No fallback. No graceful degradation. A hard error: &lt;code&gt;client version 1.24 is too old&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Why Traefik specifically exploded&lt;/h3&gt;
&lt;p&gt;Traefik uses Docker&apos;s official Go SDK (&lt;code&gt;github.com/docker/docker/client&lt;/code&gt;) for container discovery. It reads container labels, figures out routing rules, and dynamically updates its configuration.&lt;/p&gt;
&lt;p&gt;The problem is in how the Go Docker SDK initializes. When you create a new client with &lt;code&gt;client.NewClientWithOpts()&lt;/code&gt;, the SDK defaults to negotiating from API v1.24. This has been the SDK&apos;s default since it was written. It was never a problem because Docker always accepted v1.24.&lt;/p&gt;
&lt;p&gt;Docker 29 stopped accepting it. The negotiation fails before Traefik can even list containers. No containers discovered means no routes configured means every request hits a 404 or 502.&lt;/p&gt;
&lt;p&gt;This affected &lt;strong&gt;all&lt;/strong&gt; Traefik versions through v3.6.0, including v2.x. Traefik was using an SDK default (API v1.24) that Docker 29 no longer accepts.&lt;/p&gt;
&lt;h2&gt;It&apos;s Not Just Traefik&lt;/h2&gt;
&lt;p&gt;Traefik got the most attention because a broken reverse proxy takes down everything behind it. But Docker 29&apos;s API floor broke a wide range of tools:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What Broke&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traefik&lt;/strong&gt; (v3.6.0 and earlier)&lt;/td&gt;
&lt;td&gt;Container discovery, all routing&lt;/td&gt;
&lt;td&gt;Update to v3.6.1+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Portainer&lt;/strong&gt; (before v2.33.5)&lt;/td&gt;
&lt;td&gt;Can&apos;t connect to Docker environments&lt;/td&gt;
&lt;td&gt;Update to v2.33.5+ (LTS) or v2.36.0+ (STS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Watchtower&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Can&apos;t query containers for updates&lt;/td&gt;
&lt;td&gt;Switch to &lt;code&gt;nickfedor/watchtower&lt;/code&gt; (original &lt;code&gt;containrrr/watchtower&lt;/code&gt; is unmaintained)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LazyDocker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker connection fails&lt;/td&gt;
&lt;td&gt;Update to v0.24.2+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dokploy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik fails&lt;/td&gt;
&lt;td&gt;Update Traefik image to v3.6.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Coolify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik fails&lt;/td&gt;
&lt;td&gt;Check for Coolify update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Appwrite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bundled Traefik v2.11 fails&lt;/td&gt;
&lt;td&gt;Update to Appwrite version using Traefik v3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;JetBrains IDEs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker plugin throws 400 errors&lt;/td&gt;
&lt;td&gt;Update IDE / Docker plugin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spring Boot&lt;/strong&gt; (Testcontainers)&lt;/td&gt;
&lt;td&gt;docker-java defaults to API v1.32&lt;/td&gt;
&lt;td&gt;Update to Spring Boot 3.5.8+ (includes workaround), or set &lt;code&gt;api.version=1.44&lt;/code&gt; in &lt;code&gt;docker-java.properties&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Swarmpit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API calls fail&lt;/td&gt;
&lt;td&gt;No fix: project is archived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CapRover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API connectivity&lt;/td&gt;
&lt;td&gt;Check for update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CasaOS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker API connectivity&lt;/td&gt;
&lt;td&gt;Check for update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cAdvisor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metrics collection stops&lt;/td&gt;
&lt;td&gt;Update to v0.53.0+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The pattern repeats across every entry: the tool uses a Docker SDK with a hardcoded minimum version below 1.44, Docker 29 rejects the connection, and the tool can&apos;t communicate with the daemon.&lt;/p&gt;
&lt;p&gt;Swarmpit is the saddest entry on that list. The project is archived. No fix is coming. Docker 29 finished what abandonment started.&lt;/p&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;For the record, here&apos;s how this played out:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nov 10, 2025&lt;/strong&gt; — Docker Engine v29.0.0 released. Minimum API version raised to v1.44. Traefik immediately breaks for everyone who auto-updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 10-11, 2025&lt;/strong&gt; — Reports flood in. Traefik GitHub issue &lt;a href=&quot;https://github.com/traefik/traefik/issues/12253&quot;&gt;#12253&lt;/a&gt; is filed. The Docker Community Forums &lt;a href=&quot;https://forums.docker.com/t/docker-29-increased-minimum-api-version-breaks-traefik-reverse-proxy/150384&quot;&gt;thread&lt;/a&gt; explodes. The Traefik community forum fills up. Coolify, Dokploy, and Appwrite all see the same issue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 11, 2025&lt;/strong&gt; — Community identifies the &lt;code&gt;daemon.json&lt;/code&gt; workaround and the &lt;code&gt;DOCKER_MIN_API_VERSION&lt;/code&gt; environment variable as immediate workarounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 12, 2025&lt;/strong&gt; — Traefik maintainer &lt;a href=&quot;https://github.com/traefik/traefik/pull/12256&quot;&gt;PR #12256&lt;/a&gt; adds automatic API version negotiation. Community test builds appear on Docker Hub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 13-14, 2025&lt;/strong&gt; — Traefik v3.6.1 released with the fix. Portainer ships v2.33.5 and v2.36.0 with updated API support. LazyDocker ships v0.24.2.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nov 2025 onward&lt;/strong&gt; — The rest of the tooling world catches up. Spring Boot, JetBrains, and others ship fixes over the following weeks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From &quot;everything is broken&quot; to &quot;official fix available&quot;: roughly &lt;strong&gt;48 hours&lt;/strong&gt;. Solid open-source incident response. But if you were one of the people whose services went down on a Sunday night, it probably felt a lot longer.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;h3&gt;For your infrastructure&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Pin your Docker version in production.&lt;/strong&gt; Docker 29&apos;s release notes documented the API version change. But if Docker auto-updates via your package manager (as it does on Ubuntu with the official repository), you get the breaking change whether you read the notes or not.&lt;/p&gt;
&lt;p&gt;On Ubuntu/Debian:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-mark hold docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you&apos;re ready to upgrade, unpin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-mark unhold docker-ce docker-ce-cli containerd.io
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pin your Traefik version too.&lt;/strong&gt; Using &lt;code&gt;traefik:latest&lt;/code&gt; means you get updates automatically, which is usually fine, but it also means you can&apos;t predict when behavior changes. Use explicit version tags like &lt;code&gt;traefik:v3.6.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep a daemon.json backup.&lt;/strong&gt; This file is tiny but critical. If you have it backed up or version-controlled, you can apply the workaround in under a minute instead of googling for it.&lt;/p&gt;
&lt;h3&gt;For the broader picture&lt;/h3&gt;
&lt;p&gt;A huge number of Docker-adjacent tools depended on an API version Docker deprecated years ago. The SDK default of v1.24 was never updated because Docker was always backward-compatible. Tools that were actively maintained shipped fixes within days. Tools that weren&apos;t (Swarmpit, older CasaOS setups) are now permanently broken on Docker 29+.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;min-api-version&lt;/code&gt; escape hatch is Docker acknowledging the pain with a temporary lever. Eventually, even that setting may go away.&lt;/p&gt;
&lt;p&gt;If you maintain a tool that talks to Docker, check your SDK&apos;s default API version negotiation. If it&apos;s hardcoded to a minimum, make it dynamic. The Traefik PR that fixed this was a few dozen lines of code.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Update Traefik. Pin your Docker version. The order is up to you.&lt;/p&gt;
</content:encoded></item><item><title>Why I Built ZaString</title><link>https://corentings.dev/blog/why-i-built-zastring/</link><guid isPermaLink="true">https://corentings.dev/blog/why-i-built-zastring/</guid><description>On zero allocations, Span&lt;T&gt;, and the pursuit of performance without sacrifice.</description><pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ZaString started with a frustration. I was working on a high-throughput system in C#, and every profiler run showed the same thing: strings everywhere. Allocations piling up. The garbage collector working overtime. Not because the code was wrong, but because strings in C# are immutable and every operation creates a new one.&lt;/p&gt;
&lt;h2&gt;The Problem With Strings&lt;/h2&gt;
&lt;p&gt;Here&apos;s what I mean. Take something innocent-looking:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var label = $&quot;User {name} ({age}) logged in at {DateTime.Now:t}&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line. Clean. Readable. And underneath, it allocates. The interpolated string creates a &lt;code&gt;string&lt;/code&gt; on the heap. The &lt;code&gt;DateTime.Now:t&lt;/code&gt; formatting creates another. The &lt;code&gt;name&lt;/code&gt; substring reference adds overhead. Even something as simple as &lt;code&gt;str1 + str2&lt;/code&gt; doesn&apos;t modify in place. It allocates a brand new string with the combined content.&lt;/p&gt;
&lt;p&gt;In C#, strings are immutable. That&apos;s not a flaw. It&apos;s a deliberate design choice that makes them thread-safe and predictable. But immutability has a cost: every operation that &quot;changes&quot; a string actually creates a new one. Concatenation, formatting, trimming, splitting, they all allocate.&lt;/p&gt;
&lt;p&gt;For most code, this is fine. The garbage collector handles it. You never notice. But in hot paths like game loops, UI rendering, or request handlers processing thousands per second, those allocations add up. I was working with ImGui, where every frame builds strings for labels, tooltips, and debug output. Frame after frame, sixty times a second. The profiler wasn&apos;t subtle about it: strings dominated the allocation graph.&lt;/p&gt;
&lt;p&gt;I tried the usual fixes. &lt;code&gt;StringBuilder&lt;/code&gt; for concatenation. &lt;code&gt;string.Create()&lt;/code&gt; for pre-sized allocations. &lt;code&gt;ArrayPool&amp;lt;char&amp;gt;&lt;/code&gt; for reuse. They helped, but they all still allocated on the heap eventually. The fundamental issue was that the output was always a &lt;code&gt;string&lt;/code&gt;, and strings live on the heap.&lt;/p&gt;
&lt;p&gt;I didn&apos;t want to optimize allocation. I wanted to eliminate it.&lt;/p&gt;
&lt;h2&gt;Enter Span&amp;lt;T&amp;gt;&lt;/h2&gt;
&lt;p&gt;.NET 2.1 introduced &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;, and it changed what was possible.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt; is a view over a contiguous region of memory. It can wrap a managed array, a stack-allocated buffer, or unmanaged memory. Crucially, it&apos;s a &lt;code&gt;ref struct&lt;/code&gt;. That means it lives on the stack. It can&apos;t be boxed, can&apos;t be stored in a field, can&apos;t be used across async boundaries. The runtime enforces this at compile time.&lt;/p&gt;
&lt;p&gt;What you get in exchange is zero-allocation memory access. Slicing a span doesn&apos;t copy. It just creates a new view with an adjusted offset and length. Writing into a span modifies the underlying memory directly. No GC pressure. No hidden allocations.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Span&amp;lt;char&amp;gt; buffer = stackalloc char[64];
var written = &quot;Hello&quot;.AsSpan().CopyTo(buffer);
var slice = buffer[..5]; // zero-copy view
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was the insight: instead of building strings and letting the GC clean up, write directly into a buffer you already own. The buffer can be on the stack for small strings, or pooled for larger ones. When you&apos;re done, you have a &lt;code&gt;ReadOnlySpan&amp;lt;char&amp;gt;&lt;/code&gt;, which is just a view over the result with no heap allocation at all.&lt;/p&gt;
&lt;p&gt;The catch is that working with raw spans is verbose. There&apos;s no fluent API, no formatting helpers, no interpolation. You&apos;re manually tracking offsets and calling &lt;code&gt;TryFormat&lt;/code&gt; on every value. It works, but it doesn&apos;t feel like writing C#. It feels like assembly with extra steps.&lt;/p&gt;
&lt;p&gt;I wanted the ergonomics of &lt;code&gt;StringBuilder&lt;/code&gt; with the allocation profile of &lt;code&gt;Span&amp;lt;T&amp;gt;&lt;/code&gt;. That&apos;s where ZaString came from.&lt;/p&gt;
&lt;h2&gt;The Design Philosophy&lt;/h2&gt;
&lt;p&gt;The core idea behind ZaString is simple: &lt;strong&gt;zero-allocation should feel normal.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Compare the two:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// StringBuilder — 146 ns, 480 B allocated
var sb = new StringBuilder();
sb.Append(&quot;Name: &quot;).Append(&quot;John&quot;).Append(&quot;, Age: &quot;).Append(25);
var result = sb.ToString();
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// ZaSpanStringBuilder — 115 ns, 0 B allocated
Span&amp;lt;char&amp;gt; buffer = stackalloc char[50];
var builder = ZaSpanStringBuilder.Create(buffer);
builder.Append(&quot;Name: &quot;).Append(&quot;John&quot;).Append(&quot;, Age: &quot;).Append(25);
var result = builder.AsSpan();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same shape. Same chaining. Same mental model. But the second version never touches the heap. The buffer is stack-allocated, the builder is a &lt;code&gt;ref struct&lt;/code&gt;, and &lt;code&gt;AsSpan()&lt;/code&gt; returns a view, not a copy.&lt;/p&gt;
&lt;p&gt;The fluent API was non-negotiable. Every &lt;code&gt;Append&lt;/code&gt; returns &lt;code&gt;ref this&lt;/code&gt;, so you can chain calls without creating intermediate references. It supports all the types you&apos;d expect: strings, chars, numbers, booleans, dates, with formatting and culture providers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Append(&quot;Pi: &quot;).Append(Math.PI, &quot;F2&quot;)
       .Append(&quot;, Date: &quot;).Append(DateTime.Now, &quot;yyyy-MM-dd&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C# 10&apos;s interpolated string handlers make it even cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Append($&quot;User: {name}, Age: {age}, Pi: {Math.PI:F2}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The handler intercepts the interpolation at compile time and routes each placeholder directly into the span buffer. No intermediate string. No &lt;code&gt;Format()&lt;/code&gt; call. Just writes.&lt;/p&gt;
&lt;p&gt;I also wanted safety. Raw spans are dangerous. Write past the end and you corrupt memory. ZaString provides &lt;code&gt;TryAppend&lt;/code&gt; variants that return &lt;code&gt;false&lt;/code&gt; instead of throwing when the buffer is full. &lt;code&gt;TryAppendLine&lt;/code&gt; is atomic: if there isn&apos;t room for both the content and the newline, nothing gets written. No partial state. No corruption.&lt;/p&gt;
&lt;p&gt;The library grew from there. Escape helpers for JSON, HTML, URLs, CSV. Path and query parameter builders. A pooled builder for cases where the stack isn&apos;t enough. A UTF-8 writer for scenarios that need bytes instead of chars. Each piece follows the same principle: write into a buffer you control, return a view, allocate nothing.&lt;/p&gt;
&lt;h2&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The biggest lesson was about tradeoffs. Zero-allocation is not free. It&apos;s a constraint, and constraints shape design. &lt;code&gt;ref struct&lt;/code&gt; limitations mean you can&apos;t store a builder in a field, pass it to async methods, or return it from a function that escapes the stack. You have to think about buffer sizes upfront. You lose the convenience of &lt;code&gt;string&lt;/code&gt; being everywhere in the .NET ecosystem.&lt;/p&gt;
&lt;p&gt;But those constraints also force clarity. When you have to declare your buffer size, you think about how much data you actually handle. When you can&apos;t allocate, you design around reuse. The code becomes more intentional.&lt;/p&gt;
&lt;p&gt;I also learned that zero-allocation isn&apos;t always the answer. For a one-off log message, &lt;code&gt;string interpolation&lt;/code&gt; is fine. The GC won&apos;t notice. ZaString is for the hot paths, the code that runs thousands of times per frame, per request, per second. The profiler tells you where those paths are. Trust the profiler.&lt;/p&gt;
&lt;p&gt;On API design, I found that the best abstraction is the one you forget is there. If someone can read ZaString code and not realize it&apos;s zero-allocation, that&apos;s a win. The ergonomics should be invisible. The performance should be the default, not something you opt into with ceremony.&lt;/p&gt;
&lt;p&gt;And honestly? There&apos;s a specific kind of joy in running a benchmark and seeing the allocation column read &lt;strong&gt;0 B&lt;/strong&gt;. Not &quot;reduced.&quot; Not &quot;acceptable.&quot; Zero. The garbage collector has nothing to do. The memory doesn&apos;t move. The result is just... there, in the buffer, where you put it.&lt;/p&gt;
&lt;p&gt;Sometimes the best code is the code that doesn&apos;t happen.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;ZaString is available on &lt;a href=&quot;https://github.com/corentings/zastring&quot;&gt;GitHub&lt;/a&gt; and NuGet.&lt;/p&gt;
</content:encoded></item><item><title>Generic Methods Coming to Go</title><link>https://corentings.dev/blog/generic-methods-coming-to-go/</link><guid isPermaLink="true">https://corentings.dev/blog/generic-methods-coming-to-go/</guid><description>Go just accepted the proposal for generic methods on concrete types. Here&apos;s what changes, what doesn&apos;t, and why it matters.</description><pubDate>Mon, 27 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Generic Methods Coming to Go&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot;We do not anticipate that Go will ever add generic methods.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;— Go FAQ, for roughly a decade.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That line sat in the FAQ like a monument. It wasn&apos;t a &quot;not yet.&quot; It was a &quot;never.&quot; And for years, every time someone opened an issue asking for type parameters on methods, that quote was the mic drop that ended the conversation.&lt;/p&gt;
&lt;p&gt;Then, in January 2026, Robert Griesemer opened &lt;a href=&quot;https://github.com/golang/go/issues/77273&quot;&gt;proposal #77273&lt;/a&gt;: &quot;Generic methods for concrete types.&quot; The label was added: &lt;strong&gt;Proposal-Accepted&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before you get too excited: this is an accepted proposal, not a shipped feature. You cannot use this in any Go release today.&lt;/strong&gt; That said, the path forward is clear, and understanding what&apos;s coming, and what isn&apos;t, is worth your time now.&lt;/p&gt;
&lt;h2&gt;The Status Quo: The Anti-Pattern We All Live With&lt;/h2&gt;
&lt;p&gt;Since Go 1.18 gave us generics, there&apos;s been a frustrating gap. You can write generic functions and generic types, but you can&apos;t combine them into generic &lt;em&gt;methods&lt;/em&gt;. The method set of a generic type can only contain non-generic methods.&lt;/p&gt;
&lt;p&gt;This means many post-1.18 Go codebases have functions like these scattered around:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Cache[K comparable, V any] struct {
    items map[K]V
}

// This is fine: non-generic method on a generic type.
func (c *Cache[K, V]) Get(key K) (V, bool) {
    v, ok := c.items[key]
    return v, ok
}

// But you can&apos;t do this:
// func (c *Cache[K, V]) Transform[T any](fn func(V) T) []T { ... }

// So you end up with this:
func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
    result := make([]T, 0, len(c.items))
    for _, v := range c.items {
        result = append(result, fn(v))
    }
    return result
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last function takes the receiver as its first argument. It&apos;s not a method. It doesn&apos;t show up on autocomplete, can&apos;t be chained, and breaks the left-to-right reading flow that methods provide. &lt;code&gt;cache.Transform(...)&lt;/code&gt; reads naturally; &lt;code&gt;Transform(cache, ...)&lt;/code&gt; reads like inside-out code. This isn&apos;t just aesthetics: it makes APIs harder to discover, document, and compose.&lt;/p&gt;
&lt;h2&gt;Why This Became Possible&lt;/h2&gt;
&lt;p&gt;The reason generic methods were blocked for so long wasn&apos;t technical laziness. It was a genuine design problem.&lt;/p&gt;
&lt;p&gt;The original Type Parameters proposal discussed generic methods and rejected them. The reasoning went like this: if methods can have type parameters, then interface methods should too. And generic interface methods are genuinely hard. They break type erasure, create dynamic dispatch problems, and don&apos;t play well with reflection. So the whole idea was shelved.&lt;/p&gt;
&lt;p&gt;What changed? Griesemer&apos;s insight, captured in the proposal, is disarmingly simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&quot;Concrete methods are a language feature that is useful in itself, irrespective of interfaces.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Methods do two things: they implement interfaces, &lt;em&gt;and&lt;/em&gt; they organize code on a type. The Go team had been conflating these two roles. Once you separate them, the problem dissolves:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This is what&apos;s being added (concrete type, generic method):
func (s *MySlice[E]) Map[T any](fn func(E) T) []T

// This is NOT being added (interface method with type parameters):
type Reader[E any] interface {
    Read[E]()  // Still impossible
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first one is straightforward. The compiler knows the concrete type at compile time, so it can statically resolve the method using the existing type argument mechanism. No dynamic dispatch, no reflection, no runtime complexity.&lt;/p&gt;
&lt;p&gt;The second one remains unsolved because &lt;code&gt;Reader[E]&lt;/code&gt; doesn&apos;t name a single method. It names a family of methods, and Go&apos;s interface satisfaction model can&apos;t handle that.&lt;/p&gt;
&lt;p&gt;This decoupling is the key insight. Generic methods on concrete types were always implementable. They were just held hostage to the harder problem of generic interface methods.&lt;/p&gt;
&lt;p&gt;The history matters here. &lt;a href=&quot;https://github.com/golang/go/issues/49085&quot;&gt;Issue #49085&lt;/a&gt;, opened in October 2021 by the community, accumulated over 900 positive reactions. It was the primary pressure point. &lt;a href=&quot;https://github.com/golang/go/issues/50981&quot;&gt;Issue #50981&lt;/a&gt; followed in February 2022 with simpler motivating examples. Both were effectively deferred. Until now.&lt;/p&gt;
&lt;h2&gt;The Syntax&lt;/h2&gt;
&lt;p&gt;The grammar change is minimal. The method declaration production gains an optional type parameter list:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MethodDecl = &quot;func&quot; Receiver MethodName [ TypeParameters ] Signature [ FunctionBody ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In practice:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Stack[T any] struct {
    items []T
}

// Regular method on a generic type (valid since Go 1.18):
func (s *Stack[T]) Push(items ...T) {
    s.items = append(s.items, items...)
}

// Generic method with its own type parameter: new!
func (s *Stack[T]) Map[U any](fn func(T) U) *Stack[U] {
    result := &amp;amp;Stack[U]{}
    for _, item := range s.items {
        result.Push(fn(item))
    }
    return result
}

// Calling it:
nums := &amp;amp;Stack[int]{}
nums.Push(1, 2, 3)
strs := nums.Map[string](func(n int) string { return strconv.Itoa(n) })

// Type inference works:
strs = nums.Map(strconv.Itoa)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There&apos;s a subtler grammar change too: &lt;code&gt;TypeArgs&lt;/code&gt; moves from &lt;code&gt;Operand&lt;/code&gt; to &lt;code&gt;PrimaryExpr&lt;/code&gt;. This is what makes &lt;code&gt;expr.Method[T](args)&lt;/code&gt; parseable. The type arguments attach to the method call expression, not just to identifiers.&lt;/p&gt;
&lt;p&gt;Method expressions and method values work as you&apos;d expect, with one catch:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Method expression: Stack[int].Map produces a generic function
// with signature [U any](*Stack[int], func(int) U) *Stack[U]

// Method value: s.Map produces [U any] func(func(int) U) *Stack[U]

// But this is INVALID: you must instantiate the type first:
// Stack.Map[U any]  // ERROR: Stack is not instantiated
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Boundary: What This Does NOT Do&lt;/h2&gt;
&lt;p&gt;After seeing the syntax, the natural next question is: &lt;em&gt;&quot;Can I use this in interfaces?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;Generic interface methods are explicitly out of scope. This is a feature that solves the right problem first. The interface problem is a different, harder one, and leaving it unsolved here doesn&apos;t preclude a future proposal.&lt;/p&gt;
&lt;h2&gt;The Case Against&lt;/h2&gt;
&lt;p&gt;I&apos;d be dishonest if I didn&apos;t engage with the real objections.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;Go said never. Why should we trust them now?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The FAQ reversal is uncomfortable. When language designers draw a line, reversing it risks eroding trust. Some developers chose Go &lt;em&gt;because&lt;/em&gt; of its restraint. Adding features incrementally is one thing; reversing explicit &quot;never&quot; statements is another.&lt;/p&gt;
&lt;p&gt;My read: the original resistance was principled but overly conservative. The reasoning was &quot;generic methods require generic interface methods,&quot; and that premise turned out to be false. Changing course when you discover a false premise isn&apos;t flip-flopping. It&apos;s good engineering. The Go team has a track record of shipping minimal, well-thought-out language changes. I&apos;d rather they correct a mistake than defend it out of pride.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;This adds complexity for marginal benefit.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the strongest objection. Go&apos;s complexity budget is real. Every new feature makes the language harder to learn, harder to tool, and harder to reason about. Is the convenience of &lt;code&gt;cache.Transform(...)&lt;/code&gt; worth the cognitive cost of another grammar production?&lt;/p&gt;
&lt;p&gt;I think so, for two reasons. First, the implementation cost is surprisingly low. The compiler can statically resolve generic methods on concrete types without any runtime changes. This isn&apos;t adding a new dispatch mechanism; it&apos;s removing a parsing restriction. Second, the ergonomic benefit is disproportionate. I keep running into the &lt;code&gt;(receiver, typeParam)&lt;/code&gt; pattern in post-1.18 Go codebases. Fixing it at the language level eliminates a class of API awkwardness that I encounter daily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&quot;This opens the door to generic interface methods, and then we&apos;re Java.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Slippery slope arguments are easy to make. But the proposal explicitly addresses this: &lt;em&gt;&quot;It also doesn&apos;t preclude the implementation of generic interface methods at some point, should we find an acceptable implementation solution.&quot;&lt;/em&gt; This is honest. It doesn&apos;t promise never, and it doesn&apos;t promise soon. It leaves the door open for a future proposal that makes its own case.&lt;/p&gt;
&lt;p&gt;I&apos;d argue the opposite slippery slope: if Go &lt;em&gt;doesn&apos;t&lt;/em&gt; ship generic methods on concrete types, the pressure for full generic interface methods only grows. Shipping this targeted feature might actually &lt;em&gt;reduce&lt;/em&gt; demand for the more complex one by solving the practical pain point.&lt;/p&gt;
&lt;h2&gt;Practical Examples&lt;/h2&gt;
&lt;p&gt;Let me show you what this actually looks like in code you might write.&lt;/p&gt;
&lt;h3&gt;Type-Safe Builders&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;type Query[T any] struct {
    filters []func(T) bool
    limit   int
}

func NewQuery[T any]() *Query[T] {
    return &amp;amp;Query[T]{limit: -1}
}

func (q *Query[T]) Where(pred func(T) bool) *Query[T] {
    q.filters = append(q.filters, pred)
    return q
}

func (q *Query[T]) Limit(n int) *Query[T] {
    q.limit = n
    return q
}

func (q *Query[T]) Run(items []T) []T {
    // Applies all filters and returns matching items (implementation omitted)
}

// Usage: notice how the type parameter flows through the chain:
type User struct {
    Name string
    Age  int
}

users := []User{{&quot;Alice&quot;, 30}, {&quot;Bob&quot;, 25}, {&quot;Charlie&quot;, 30}}
activeAdults := NewQuery[User]().
    Where(func(u User) bool { return u.Age &amp;gt;= 18 }).
    Where(func(u User) bool { return u.Name != &quot;&quot; }).
    Limit(10).
    Run(users)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Today, you&apos;d need to make &lt;code&gt;Where&lt;/code&gt; a package-level function that takes the query as its first argument. The chaining breaks. The type parameter doesn&apos;t flow naturally. With generic methods, the builder pattern becomes first-class.&lt;/p&gt;
&lt;h3&gt;GroupBy: A Method-Level Type Parameter&lt;/h3&gt;
&lt;p&gt;Here&apos;s a case where the method genuinely needs its own type parameter, unrelated to the receiver&apos;s:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Table[R any] struct {
    rows []R
}

func (t *Table[R]) GroupBy[K comparable](keyFn func(R) K) map[K][]R {
    groups := make(map[K][]R)
    for _, row := range t.rows {
        k := keyFn(row)
        groups[k] = append(groups[k], row)
    }
    return groups
}

// Usage:
orders := &amp;amp;Table[Order]{rows: allOrders}
byCustomer := orders.GroupBy[string](func(o Order) string { return o.CustomerID })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type parameter &lt;code&gt;K&lt;/code&gt; belongs to &lt;code&gt;GroupBy&lt;/code&gt;, not to &lt;code&gt;Table&lt;/code&gt;. This is the kind of thing you simply cannot express as a method today. With generic methods, it becomes natural.&lt;/p&gt;
&lt;h3&gt;The &lt;code&gt;List[E].Format[F]&lt;/code&gt; Pattern&lt;/h3&gt;
&lt;p&gt;This is the motivating example from the proposal itself: a method that introduces its own type parameter independent of the type&apos;s parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type List[E any] struct {
    elements []E
}

func (l *List[E]) Format[F any](formatter func(E) F) []F {
    result := make([]F, len(l.elements))
    for i, e := range l.elements {
        result[i] = formatter(e)
    }
    return result
}

// E is string, F is int: two independent type parameters
names := &amp;amp;List[string]{elements: []string{&quot;hello&quot;, &quot;world&quot;}}
lengths := names.Format[int](func(s string) int { return len(s) })
// lengths == []int{5, 5}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the case that&apos;s truly impossible to express cleanly today. The method needs a type parameter (&lt;code&gt;F&lt;/code&gt;) that&apos;s unrelated to the type&apos;s parameter (&lt;code&gt;E&lt;/code&gt;). A package-level function can do it, but then you lose the method call syntax entirely.&lt;/p&gt;
&lt;h2&gt;How to Prepare Today&lt;/h2&gt;
&lt;p&gt;You can&apos;t use generic methods yet, but you can write code that migrates trivially when they land.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Wrap your &lt;code&gt;(receiver, typeParam)&lt;/code&gt; functions on types.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Transform[K comparable, V any, T any](c *Cache[K, V], fn func(V) T) []T {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the type as the first argument. When generic methods ship, the migration is mechanical: move the function onto the type, drop the receiver parameter, done. The function body doesn&apos;t change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Stop reaching for &lt;code&gt;interface{}&lt;/code&gt; workarounds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you&apos;re using empty interfaces to work around the inability to write generic methods, stop. Design your types with proper type parameters now. The code will work today with package-level functions and become cleaner tomorrow with methods.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Name your helper functions after the future method.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you plan to add a &lt;code&gt;Transform&lt;/code&gt; method to your &lt;code&gt;Cache&lt;/code&gt; type, name the package-level function &lt;code&gt;Transform&lt;/code&gt;, not &lt;code&gt;TransformCache&lt;/code&gt; or &lt;code&gt;ApplyToCache&lt;/code&gt;. Mechanical migration depends on name alignment.&lt;/p&gt;
&lt;h2&gt;When Will It Land?&lt;/h2&gt;
&lt;p&gt;Here&apos;s where things stand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Parser:&lt;/strong&gt; Already handles type parameters on methods (parses them, currently rejects with an error). The change to accept them is trivial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type checker:&lt;/strong&gt; Needs modifications to remove the current restriction and handle method expressions/values with type parameters. In progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compiler backend:&lt;/strong&gt; Generic method calls on concrete types can be statically resolved and rewritten as generic function calls. This is well-understood: no new dispatch mechanism needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export/import format:&lt;/strong&gt; This is the hardest part. The serialized format for compiled packages needs to represent generic methods, and changing it affects tooling across the ecosystem (gopls, vulncheck, build cache compatibility). This is what determines the timeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;x/tools&lt;/code&gt; repository already has &lt;a href=&quot;https://github.com/golang/go/issues/77549&quot;&gt;a tracking issue (#77549)&lt;/a&gt; for generic method support across the toolchain. The Go team typically allows one or two release cycles for tooling to catch up after a language change.&lt;/p&gt;
&lt;p&gt;Go 1.27 or 1.28 feels realistic. This is a well-scoped change with a clear implementation path, not a years-long research project like the original generics design.&lt;/p&gt;
&lt;h2&gt;My Take&lt;/h2&gt;
&lt;p&gt;I&apos;m genuinely excited about this, and not just because I get to delete a category of awkward functions from my codebases.&lt;/p&gt;
&lt;p&gt;What I appreciate about this proposal is the discipline. The Go team could have swung for the fences and tried to solve generic interface methods too. Instead, they identified the part that&apos;s well-understood, implementable, and high-value, and they scoped &lt;em&gt;just that&lt;/em&gt;. That&apos;s the kind of restraint that made Go worth using in the first place.&lt;/p&gt;
&lt;p&gt;What I&apos;d warn against: don&apos;t use generic methods as a hammer. If a package-level generic function reads clearly, keep it as a function. Methods aren&apos;t inherently better. They&apos;re better when they improve API discoverability and composability. Use the feature where it earns its complexity budget, not everywhere it&apos;s syntactically legal.&lt;/p&gt;
&lt;p&gt;Go&apos;s relationship with generics has been cautious, sometimes frustratingly so. But this proposal feels like the right next step: small, principled, and immediately useful. The FAQ was wrong. That&apos;s okay. Recognizing a mistake and correcting it is better than defending it indefinitely.&lt;/p&gt;
</content:encoded></item><item><title>I Write, I Code, I Explore — Why Verbs, Not Nouns</title><link>https://corentings.dev/blog/verbs-not-nouns/</link><guid isPermaLink="true">https://corentings.dev/blog/verbs-not-nouns/</guid><description>On defining yourself by what you do, not what you are.</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was listening to one of my favorite episodes of the ChessMood Podcast from my friend GM Avetik, &lt;a href=&quot;https://www.youtube.com/watch?v=pVHyUzX5IQw&amp;amp;t=4011s&quot;&gt;How you can perform at your peak like top athletes do | Todd Herman on ChessMood Podcast&lt;/a&gt;, and around 01h07 one small idea from it kept following me around.&lt;/p&gt;
&lt;p&gt;He said he prefers verbs to nouns when he talks about himself.&lt;/p&gt;
&lt;p&gt;Not &quot;I&apos;m a coach,&quot; but &quot;I coach.&quot;
Not &quot;I&apos;m an entrepreneur,&quot; but &quot;I build.&quot;
Not &quot;I&apos;m an author,&quot; but &quot;I write.&quot;&lt;/p&gt;
&lt;p&gt;The more I sat with that, the more it explained a tension I&apos;ve felt for years.&lt;/p&gt;
&lt;p&gt;There are days when &quot;developer&quot; feels too heavy a word for me. I can ship something useful in the morning, get stuck on a dumb bug in the afternoon, and end the day feeling like I should hand back the badge. The same thing happens with writing. If I tell myself &quot;I am a writer,&quot; every weak paragraph feels like evidence against me. But if I say &quot;I write,&quot; the whole thing softens. I wrote today. Some of it was good. Some of it wasn&apos;t. Both can be true.&lt;/p&gt;
&lt;p&gt;That&apos;s what I like about verbs: they leave room for motion.&lt;/p&gt;
&lt;p&gt;Nouns can be useful. They help other people understand roughly where you live in the world. But they also freeze you at strange moments. They can turn yesterday&apos;s result into today&apos;s identity. &quot;I am this kind of person.&quot; &quot;I am this level.&quot; &quot;I am this role.&quot; And once that sentence hardens, you start protecting it. You hesitate to be bad at something new because it might threaten the name you&apos;ve chosen for yourself.&lt;/p&gt;
&lt;p&gt;Verbs don&apos;t ask for that kind of defense. Verbs ask for practice.&lt;/p&gt;
&lt;p&gt;&quot;I write&quot; means I can write something sharp one day and clumsy the next. &quot;I code&quot; means I can build a feature, break something obvious, learn, fix it, and keep going. The verb doesn&apos;t collapse because the performance wasn&apos;t perfect. In a quiet way, it&apos;s a more merciful grammar.&lt;/p&gt;
&lt;p&gt;What stayed with me most from Todd Herman&apos;s framing was that this changes across contexts. The verb that describes how I work is not always the same one that describes how I love people. In one part of life, maybe I build. In another, I listen. In another, I encourage. The noun tries to gather everything into one static identity. The verb asks a better question: what am I actually doing here?&lt;/p&gt;
&lt;p&gt;That question feels closer to how life is really lived.&lt;/p&gt;
&lt;p&gt;I don&apos;t want to be trapped by a title that only describes me on my best days. I&apos;d rather use words that still fit on ordinary days. I wrote bad code today. I rewrote a paragraph five times. I explored an idea without being sure it would go anywhere. None of that cancels the practice. It is the practice.&lt;/p&gt;
&lt;p&gt;And there is something quietly reassuring in that. If a title disappears, the verb often remains. If one day I stop calling myself a developer, I can still build. If I stop calling myself a writer, I can still write. The label may change with the season. The work can keep moving.&lt;/p&gt;
&lt;p&gt;So I find myself trusting verbs more.&lt;/p&gt;
&lt;p&gt;Not because nouns are evil, and not because language alone can save us, but because verbs keep me closer to the living part of things. They remind me that I am not a finished category. I am a person in motion, practicing.&lt;/p&gt;
</content:encoded></item><item><title>Go Pipeline Pattern: Turning Streams into Useful Data</title><link>https://corentings.dev/blog/go-pattern-pipeline/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-pipeline/</guid><description>Learn the Pipeline Pattern in Go using goroutines and channels. Build composable stages for parsing, filtering, enriching, and processing log streams.</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Pipeline Pattern in Go&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Sometimes, the hard part of concurrent programming is not making things run in parallel.
The hard part is keeping the flow of data understandable.&lt;/p&gt;
&lt;p&gt;A pipeline is a simple way to do that.
Instead of putting every responsibility inside one large loop, you split the work into small stages.
Each stage receives values from a channel, does one transformation, and sends the result to the next stage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source -&amp;gt; parse -&amp;gt; filter -&amp;gt; enrich -&amp;gt; sink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Go, this pattern feels natural because goroutines and channels already give us the building blocks.
A generator can create the initial stream, each pipeline stage can transform it, and a final consumer can collect or print the result.&lt;/p&gt;
&lt;p&gt;This article continues the &lt;strong&gt;Go Patterns&lt;/strong&gt; series after Producer-Consumer, Generator, and Worker Pool.
The goal is not to build a framework.
The goal is to learn how to structure data processing without turning one function into a pile of responsibilities.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;p&gt;Use the Pipeline Pattern when data moves through multiple steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Processing log streams&lt;/li&gt;
&lt;li&gt;Reading, validating, and transforming CSV rows&lt;/li&gt;
&lt;li&gt;Cleaning API responses before storing them&lt;/li&gt;
&lt;li&gt;Building small ETL-like flows&lt;/li&gt;
&lt;li&gt;Splitting parsing, filtering, enrichment, and reporting into separate responsibilities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is especially useful when each step can be described as:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;take a stream of values, transform or filter it, and return another stream of values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Why Use It&lt;/h2&gt;
&lt;p&gt;Pipelines are useful because they keep each stage focused.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Separation of concerns&lt;/strong&gt;: parsing, filtering, and reporting do not live in the same loop&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composability&lt;/strong&gt;: stages can be reused and reordered&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Natural backpressure&lt;/strong&gt;: channels slow down upstream stages when downstream stages cannot keep up&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: each stage can be tested with a small input channel&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Readable concurrency&lt;/strong&gt;: the data flow is visible from the stage composition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pattern is not magic.
It is mostly discipline: one stage, one responsibility.&lt;/p&gt;
&lt;h2&gt;How It Works&lt;/h2&gt;
&lt;p&gt;A pipeline stage usually has this shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func stage(in &amp;lt;-chan Input) &amp;lt;-chan Output {
    out := make(chan Output)

    go func() {
        defer close(out)

        for value := range in {
            out &amp;lt;- transform(value)
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stage receives a read-only channel, creates its own output channel, starts a goroutine, then closes the output channel when the input channel is exhausted.&lt;/p&gt;
&lt;p&gt;This gives us a chain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw := source()
parsed := parse(raw)
filtered := filter(parsed)
enriched := enrich(filtered)
sink(enriched)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;p&gt;Before looking at logs, let&apos;s start with a tiny pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;numbers -&amp;gt; square -&amp;gt; keep even -&amp;gt; print
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;fmt&quot;

func numbers(max int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for i := 1; i &amp;lt;= max; i++ {
            out &amp;lt;- i
        }
    }()

    return out
}

func square(in &amp;lt;-chan int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for n := range in {
            out &amp;lt;- n * n
        }
    }()

    return out
}

func keepEven(in &amp;lt;-chan int) &amp;lt;-chan int {
    out := make(chan int)

    go func() {
        defer close(out)

        for n := range in {
            if n%2 == 0 {
                out &amp;lt;- n
            }
        }
    }()

    return out
}

func main() {
    values := numbers(10)
    squared := square(values)
    evenSquares := keepEven(squared)

    for n := range evenSquares {
        fmt.Println(n)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each function owns one small part of the work.
&lt;code&gt;numbers&lt;/code&gt; produces values, &lt;code&gt;square&lt;/code&gt; transforms them, &lt;code&gt;keepEven&lt;/code&gt; filters them, and &lt;code&gt;main&lt;/code&gt; consumes the final stream.&lt;/p&gt;
&lt;p&gt;That is the pipeline pattern in its smallest useful form.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Processing Pipeline&lt;/h2&gt;
&lt;p&gt;Now let&apos;s use a more realistic example.&lt;/p&gt;
&lt;p&gt;Imagine we receive raw log lines and want to turn them into useful information.
We can model that as a pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;raw log lines -&amp;gt; parse logs -&amp;gt; filter errors -&amp;gt; enrich logs -&amp;gt; print report
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a complete runnable version, this example needs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import (
    &quot;fmt&quot;
    &quot;strings&quot;
    &quot;time&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, define the data we want to pass between stages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type RawLog string

type LogEntry struct {
    Timestamp time.Time
    Level     string
    Service   string
    Message   string
    Alert     bool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The source stage sends raw log lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func logSource(lines []string) &amp;lt;-chan RawLog {
    out := make(chan RawLog)

    go func() {
        defer close(out)

        for _, line := range lines {
            out &amp;lt;- RawLog(line)
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parser turns each raw line into a structured &lt;code&gt;LogEntry&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func parseLogs(in &amp;lt;-chan RawLog) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for raw := range in {
            parts := strings.SplitN(string(raw), &quot;|&quot;, 4)
            if len(parts) != 4 {
                continue
            }

            timestamp, err := time.Parse(time.RFC3339, parts[0])
            if err != nil {
                continue
            }

            out &amp;lt;- LogEntry{
                Timestamp: timestamp,
                Level:     parts[1],
                Service:   parts[2],
                Message:   parts[3],
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The filter stage keeps only errors:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func filterErrors(in &amp;lt;-chan LogEntry) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for entry := range in {
            if entry.Level == &quot;ERROR&quot; {
                out &amp;lt;- entry
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The enrichment stage adds a small piece of derived information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func enrichLogs(in &amp;lt;-chan LogEntry) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for entry := range in {
            entry.Alert = entry.Service == &quot;payment&quot; || entry.Service == &quot;auth&quot;
            out &amp;lt;- entry
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, the sink consumes the enriched entries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func printReport(in &amp;lt;-chan LogEntry) {
    for entry := range in {
        alert := &quot;&quot;
        if entry.Alert {
            alert = &quot; [ALERT]&quot;
        }

        fmt.Printf(&quot;%s %s: %s%s\n&quot;, entry.Service, entry.Level, entry.Message, alert)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full pipeline becomes very readable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func main() {
    lines := []string{
        &quot;2026-04-24T10:00:00Z|INFO|api|request completed&quot;,
        &quot;2026-04-24T10:00:01Z|ERROR|payment|card authorization failed&quot;,
        &quot;2026-04-24T10:00:02Z|ERROR|worker|job timeout&quot;,
        &quot;2026-04-24T10:00:03Z|ERROR|auth|invalid token&quot;,
    }

    raw := logSource(lines)
    parsed := parseLogs(raw)
    errors := filterErrors(parsed)
    enriched := enrichLogs(errors)

    printReport(enriched)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is not the log format.
The important part is that the flow is explicit.&lt;/p&gt;
&lt;p&gt;Each stage can be read, tested, and replaced independently.&lt;/p&gt;
&lt;h2&gt;Error Handling&lt;/h2&gt;
&lt;p&gt;The log parser above silently skips invalid lines.
That keeps the example small, but it is not always what you want in production.&lt;/p&gt;
&lt;p&gt;Two common approaches are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send errors to a separate error channel&lt;/li&gt;
&lt;li&gt;Pass a result type through the pipeline&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogResult struct {
    Entry LogEntry
    Err   error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes failures explicit without panicking inside a goroutine.
It also lets the final consumer decide whether to log, count, retry, or ignore invalid records.&lt;/p&gt;
&lt;h2&gt;Cancellation&lt;/h2&gt;
&lt;p&gt;The examples above work for finite inputs.
For long-running pipelines, use &lt;code&gt;context.Context&lt;/code&gt; so every stage can stop when the caller is done.&lt;/p&gt;
&lt;p&gt;The shape usually looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func parseLogs(ctx context.Context, in &amp;lt;-chan RawLog) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)

    go func() {
        defer close(out)

        for {
            select {
            case &amp;lt;-ctx.Done():
                return
            case raw, ok := &amp;lt;-in:
                if !ok {
                    return
                }

                entry, ok := parseLog(raw)
                if ok {
                    out &amp;lt;- entry
                }
            }
        }
    }()

    return out
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without cancellation, a pipeline that reads from a never-ending source can leak goroutines when the consumer stops early.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep each stage focused on one responsibility&lt;/li&gt;
&lt;li&gt;Return receive-only channels (&lt;code&gt;&amp;lt;-chan T&lt;/code&gt;) from stages&lt;/li&gt;
&lt;li&gt;Close the output channel from the goroutine that writes to it&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;context.Context&lt;/code&gt; for long-running or cancellable pipelines&lt;/li&gt;
&lt;li&gt;Test each stage independently with small input channels&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close output channels&lt;/li&gt;
&lt;li&gt;Stopping early without cancelling upstream stages&lt;/li&gt;
&lt;li&gt;Creating too many tiny stages that hide simple logic&lt;/li&gt;
&lt;li&gt;Mixing parsing, filtering, enrichment, and reporting in one function&lt;/li&gt;
&lt;li&gt;Assuming ordering will stay the same if you later parallelize a stage&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Generator Pattern: Creates the initial stream of values&lt;/li&gt;
&lt;li&gt;Producer-Consumer Pattern: Separates production from consumption&lt;/li&gt;
&lt;li&gt;Worker Pool Pattern: Parallelizes expensive stages&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: Distributes one stage across multiple workers and merges the results&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Pipeline Pattern is one of the most readable ways to structure data processing in Go.
It lets you split a flow into small stages, connect them with channels, and keep each responsibility isolated.&lt;/p&gt;
&lt;p&gt;It works well when data naturally moves through a sequence: read, parse, filter, enrich, report.&lt;/p&gt;
&lt;p&gt;The pattern is also a bridge to more advanced concurrency designs.
Once one stage becomes too slow, you can combine a pipeline with a Worker Pool or Fan-Out/Fan-In to parallelize only that part of the flow.&lt;/p&gt;
&lt;h2&gt;Testing Pipelines&lt;/h2&gt;
&lt;p&gt;The best pipeline tests treat each stage as a pure function: send a channel in, get a channel out. The goroutines are implementation details. I wrote about that approach in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Flexible Approaches to Worker Pools in Go</title><link>https://corentings.dev/blog/semaphore-pattern-worker/</link><guid isPermaLink="true">https://corentings.dev/blog/semaphore-pattern-worker/</guid><description>Explore flexible approaches to the Worker Pool pattern in Go, including the Shared Semaphore method and third-party libraries. Learn when to use each approach for optimal concurrency management in your Go projects.</description><pubDate>Thu, 12 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Flexible Approaches to Worker Pools in Go&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;While the standard Worker Pool pattern is powerful, Go&apos;s flexibility allows for alternative approaches to concurrent task processing. This article explores the Shared Semaphore method and discusses the use of third-party libraries for managing concurrency in Go applications.&lt;/p&gt;
&lt;h2&gt;Shared Semaphore Method&lt;/h2&gt;
&lt;p&gt;The Shared Semaphore method uses a buffered channel as a semaphore to limit concurrency across various parts of an application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

func processWithSemaphore(tasks []int, maxConcurrency int) {
	sem := make(chan struct{}, maxConcurrency)
	var wg sync.WaitGroup

	for _, task := range tasks {
		wg.Add(1)
		sem &amp;lt;- struct{}{} // Acquire semaphore
		go func(task int) {
			defer wg.Done()
			defer func() { &amp;lt;-sem }() // Release semaphore
			processTask(task)
		}(task)
	}

	wg.Wait()
}

func processTask(task int) {
	fmt.Printf(&quot;Processing task %d\n&quot;, task)
	time.Sleep(time.Second) // Simulate work
	fmt.Printf(&quot;Completed task %d\n&quot;, task)
}

func main() {
	tasks := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	processWithSemaphore(tasks, 3)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Advantages:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Simple and lightweight implementation&lt;/li&gt;
&lt;li&gt;Scales to zero when not in use&lt;/li&gt;
&lt;li&gt;Can be used in multiple places without increasing complexity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Use Case:&lt;/h3&gt;
&lt;p&gt;Ideal for scenarios where you need to limit concurrency across various parts of your application without maintaining a persistent worker pool.&lt;/p&gt;
&lt;h2&gt;Third-Party Libraries&lt;/h2&gt;
&lt;p&gt;While implementing your own worker pool is often the best approach, there are some third-party libraries that can be useful in certain situations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ants&lt;/strong&gt;: A high-performance goroutine pool in Go, providing features like automatic scaling and reuse of goroutines.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;sourcegraph/conc&lt;/strong&gt;: A package for structured concurrency in Go, offering higher-level abstractions for common concurrency patterns.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These libraries can be beneficial when you need advanced features or when working on complex concurrent systems. However, it&apos;s important to note that in approximately 90% of cases, maintaining your own worker pool implementation is preferable. This approach gives you more control, better understanding of your concurrency model, and avoids unnecessary dependencies.&lt;/p&gt;
&lt;h2&gt;Choosing the Right Approach&lt;/h2&gt;
&lt;p&gt;Consider the following factors when deciding on your concurrency approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Assess your concurrency needs:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Do you need to limit concurrency across multiple parts of your application?&lt;/li&gt;
&lt;li&gt;Is your workload consistent or variable?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Evaluate your task characteristics:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Are tasks short-lived or long-running?&lt;/li&gt;
&lt;li&gt;Do you need fine-grained control over task execution?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Consider your application architecture:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Is simplicity a priority?&lt;/li&gt;
&lt;li&gt;Do you need to scale workers dynamically?&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Decision tree:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;If (need to limit concurrency in specific sections) AND (variable workload):
Use Shared Semaphore&lt;/li&gt;
&lt;li&gt;Else if (consistent workload) AND (need fine-grained control):
Use Standard Worker Pool&lt;/li&gt;
&lt;li&gt;Else if (need advanced features) AND (complexity is justified):
Consider third-party libraries like Ants or sourcegraph/conc&lt;/li&gt;
&lt;li&gt;Else:
Implement and maintain your own worker pool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This decision process helps guide your choice based on your specific context and requirements, ensuring you choose the most appropriate concurrency pattern for your needs while prioritizing simplicity and control in most cases.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;While the standard Worker Pool pattern is versatile, Go&apos;s concurrency model allows for flexible approaches like the Shared Semaphore method. This alternative can be particularly useful for applications with varying concurrency needs across different components. Third-party libraries offer advanced features but should be used judiciously, as maintaining your own worker pool often provides the best balance of control and simplicity.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an overview of flexible approaches to worker pools in Go. While these patterns are powerful, it&apos;s important to consider the specific needs of your application when implementing them. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Mastering the Worker Pool Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-worker/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-worker/</guid><description>Master the Worker Pool Pattern in Go to manage concurrent tasks efficiently. Control resource usage, improve throughput, and scale your applications.</description><pubDate>Tue, 10 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Worker Pool Pattern in Go&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern is a fundamental concurrency design in Go that efficiently manages a pool of worker goroutines to process tasks from a shared queue. This pattern excels at handling a large number of independent tasks concurrently while maintaining precise control over system resources and performance.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Processing a large number of independent tasks that can be parallelized&lt;/li&gt;
&lt;li&gt;Limiting the number of concurrent operations to prevent resource exhaustion&lt;/li&gt;
&lt;li&gt;Balancing workload across multiple processors or cores&lt;/li&gt;
&lt;li&gt;Managing CPU-bound or I/O-bound tasks efficiently&lt;/li&gt;
&lt;li&gt;Handling batch processing operations with controlled parallelism&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Controls resource utilization by maintaining a fixed number of workers&lt;/li&gt;
&lt;li&gt;Improves performance through efficient parallel processing&lt;/li&gt;
&lt;li&gt;Prevents system overload by limiting concurrent operations&lt;/li&gt;
&lt;li&gt;Enhances application scalability and throughput&lt;/li&gt;
&lt;li&gt;Maintains predictable resource usage patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern consists of three essential components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A pool of worker goroutines that process tasks concurrently&lt;/li&gt;
&lt;li&gt;A job queue (input channel) that holds pending tasks&lt;/li&gt;
&lt;li&gt;A results queue (output channel) that collects processed results&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Workers continuously pull tasks from the job queue, process them independently, and send results to the results queue. This design ensures efficient task distribution and controlled concurrency.&lt;/p&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func worker(id int, jobs &amp;lt;-chan int, results chan&amp;lt;- int) {
    for job := range jobs {
        fmt.Printf(&quot;Worker %d processing job %d\n&quot;, id, job)
        time.Sleep(time.Second) // Simulating work
        results &amp;lt;- job * 2
    }
}

func main() {
    const numJobs = 5
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Start worker pool
    for w := 1; w &amp;lt;= numWorkers; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs to the workers
    for j := 1; j &amp;lt;= numJobs; j++ {
        jobs &amp;lt;- j
    }
    close(jobs)

    // Collect and print results
    for a := 1; a &amp;lt;= numJobs; a++ {
        result := &amp;lt;-results
        fmt.Printf(&quot;Job result: %d\n&quot;, result)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Example: Image Processor&lt;/h2&gt;
&lt;p&gt;Let&apos;s consider a scenario where we need to process multiple images concurrently:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Job struct {
    ID       int
    ImageURL string
    Size     int
}

type Result struct {
    JobID     int
    ImageURL  string
    NewSize   int
    Error     error
    TimeSpent time.Duration
}

func imageProcessor(id int, jobs &amp;lt;-chan Job, results chan&amp;lt;- Result) {
    for job := range jobs {
        startTime := time.Now()

        fmt.Printf(&quot;Worker %d processing image %d from %s\n&quot;, id, job.ID, job.ImageURL)

        result := Result{
            JobID:    job.ID,
            ImageURL: job.ImageURL,
            NewSize:  job.Size,
        }

        // Simulate image processing with realistic steps
        err := processImage(job)
        if err != nil {
            result.Error = err
            results &amp;lt;- result
            continue
        }

        result.TimeSpent = time.Since(startTime)
        results &amp;lt;- result
    }
}

func processImage(job Job) error {
    // Simulate various image processing steps
    time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)

    // Simulate potential errors
    if rand.Float32() &amp;lt; 0.1 {
        return fmt.Errorf(&quot;failed to process image %d: simulation error&quot;, job.ID)
    }

    return nil
}

func main() {
    numCPU := runtime.NumCPU()
    runtime.GOMAXPROCS(numCPU)
    numWorkers := numCPU * 2 // Use 2 workers per CPU core
    const numJobs = 10

    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)

    // Initialize worker pool
    for w := 1; w &amp;lt;= numWorkers; w++ {
        go imageProcessor(w, jobs, results)
    }

    // Send image processing jobs
    for j := 1; j &amp;lt;= numJobs; j++ {
        jobs &amp;lt;- Job{
            ID:       j,
            ImageURL: fmt.Sprintf(&quot;https://example.com/image%d.jpg&quot;, j),
            Size:     100 * j, // Varying sizes
        }
    }
    close(jobs)

    // Collect and handle results
    for a := 1; a &amp;lt;= numJobs; a++ {
        result := &amp;lt;-results
        if result.Error != nil {
            fmt.Printf(&quot;Error processing image %d: %v\n&quot;, result.JobID, result.Error)
        } else {
            fmt.Printf(&quot;Successfully processed image %d to size %dpx in %v\n&quot;,
                result.JobID, result.NewSize, result.TimeSpent)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always close the channel when generation is complete&lt;/li&gt;
&lt;li&gt;Use buffered channels when appropriate to prevent blocking&lt;/li&gt;
&lt;li&gt;Include monitoring and logging for production environments&lt;/li&gt;
&lt;li&gt;Implement graceful shutdown mechanisms&lt;/li&gt;
&lt;li&gt;Size your worker pool based on available system resources&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating too many workers, leading to resource exhaustion&lt;/li&gt;
&lt;li&gt;Not handling worker failures or panics&lt;/li&gt;
&lt;li&gt;Forgetting to close channels properly&lt;/li&gt;
&lt;li&gt;Missing timeout mechanisms for long-running tasks&lt;/li&gt;
&lt;li&gt;Inefficient job distribution strategies&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Worker Pool pattern is a powerful tool in Go&apos;s concurrency toolkit, offering a balanced approach to parallel processing. By maintaining a fixed number of workers, it prevents resource exhaustion while maximizing throughput. The pattern is particularly valuable in real-world scenarios such as image processing, batch operations, and API request handling, where controlled concurrent processing is essential for optimal performance and resource utilization.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an introduction to the Worker Pool pattern in Go. While the pattern is powerful, it&apos;s important to consider the specific needs of your application when implementing it. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;h2&gt;Testing Worker Pools&lt;/h2&gt;
&lt;p&gt;If you test a worker pool, focus on the contract: jobs go in, results come out. The number of goroutines is an implementation detail. I wrote about that in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-generator/&quot;&gt;Mastering the Generator Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-pipeline/&quot;&gt;Go Pipeline Pattern: Turning Streams into Useful Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Advanced Generator Pattern: Consuming and Testing Data Streams</title><link>https://corentings.dev/blog/advanced-real-world-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/advanced-real-world-generator/</guid><description>Advanced Generator Pattern in Go: testing, error handling, and real-world data generation techniques for robust applications.</description><pubDate>Sun, 08 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Advanced Generator Pattern: Consuming and Testing Streams&lt;/h2&gt;
&lt;p&gt;:::warning[Difficulty Level]
Advanced
:::&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Expanding on our previous discussions of the Generator pattern, we&apos;ll explore two advanced applications: consuming large datasets lazily and simulating data streams for testing. These techniques are crucial for efficient data processing and robust application testing.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Processing large datasets that don&apos;t fit in memory&lt;/li&gt;
&lt;li&gt;Simulating data sources for testing&lt;/li&gt;
&lt;li&gt;Implementing ETL (Extract, Transform, Load) processes&lt;/li&gt;
&lt;li&gt;Creating reproducible test scenarios for data processing pipelines&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory Efficiency&lt;/strong&gt;: Process large datasets without loading everything into memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testability&lt;/strong&gt;: Create controlled environments for testing data processing logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Easily switch between real and simulated data sources&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: Generate consistent test cases for data processing scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Create generator functions that yield data items one at a time&lt;/li&gt;
&lt;li&gt;Use channels to stream data from the source to the consumer&lt;/li&gt;
&lt;li&gt;Implement lazy loading for large datasets&lt;/li&gt;
&lt;li&gt;Create mock data generators for testing scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Example 1: Lazy Loading of Large Datasets&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type DataItem struct {
    ID   int
    Data string
}

// lazyDataLoader simulates loading a large dataset lazily
func lazyDataLoader(filePath string) &amp;lt;-chan DataItem {
    out := make(chan DataItem)
    go func() {
        defer close(out)
        // Simulate opening a large file
        fmt.Printf(&quot;Opening file: %s\n&quot;, filePath)

        // Simulate reading the file line by line
        for i := 0; i &amp;lt; 1000000; i++ {
            // Simulate processing delay for each item
            time.Sleep(1 * time.Millisecond)
            out &amp;lt;- DataItem{
                ID:   i + 1,
                Data: fmt.Sprintf(&quot;Data from line %d&quot;, i+1),
            }
            if i%100000 == 0 {
                fmt.Printf(&quot;Processed %d items\n&quot;, i)
            }
        }
    }()
    return out
}

func processData(data &amp;lt;-chan DataItem) {
    for item := range data {
        // Simulate data processing
        processedData := fmt.Sprintf(&quot;Processed: %s (ID: %d)&quot;, item.Data, item.ID)
        fmt.Println(processedData)
    }
}

func main() {
    dataStream := lazyDataLoader(&quot;large_dataset.txt&quot;)
    processData(dataStream)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates lazy loading of a large dataset, processing items one at a time without loading the entire dataset into memory.&lt;/p&gt;
&lt;h2&gt;Example 2: Simulating Data Streams for Testing&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type DataItem struct {
    ID   int
    Data string
}

// mockDataStream simulates a data source (e.g., a file, queue, or network stream)
func mockDataStream(count int) &amp;lt;-chan DataItem {
    out := make(chan DataItem)
    go func() {
       defer close(out)
       for i := 0; i &amp;lt; count; i++ {
          // Simulate reading from a data source
          time.Sleep(100 * time.Millisecond)
          out &amp;lt;- DataItem{
             ID:   i + 1,
             Data: fmt.Sprintf(&quot;Data-%d&quot;, i+1),
          }
       }
    }()
    return out
}

// dataGenerator consumes the mock stream and yields processed data
func dataGenerator(stream &amp;lt;-chan DataItem) &amp;lt;-chan string {
    out := make(chan string)
    go func() {
       defer close(out)
       for item := range stream {
          // Process the data item
          processedData := fmt.Sprintf(&quot;Processed: %s (ID: %d)&quot;, item.Data, item.ID)
          out &amp;lt;- processedData
       }
    }()
    return out
}

type StreamGenerator struct{}

func (g StreamGenerator) Execute() {
    // Create a mock data stream
    dataStream := mockDataStream(10)

    // Create a generator to process the stream
    processedDataGen := dataGenerator(dataStream)

    // Consume and print the processed data
    for data := range processedDataGen {
       fmt.Println(data)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates a more structured approach to using the Generator pattern for testing data processing pipelines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mockDataStream simulates a data source by generating items with controlled timing&lt;/li&gt;
&lt;li&gt;dataGenerator shows how to process a stream of data items and transform them&lt;/li&gt;
&lt;li&gt;The StreamGenerator type provides a clean interface for executing the pipeline and can be replaced with real data sources in production using DI (Dependency Injection)&lt;/li&gt;
&lt;li&gt;Each stage of the pipeline is clearly separated and testable&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use buffered channels for improved performance when processing large streams&lt;/li&gt;
&lt;li&gt;Implement timeout mechanisms for long-running operations&lt;/li&gt;
&lt;li&gt;Use the &lt;code&gt;context&lt;/code&gt; package for cancellation in long-running generators&lt;/li&gt;
&lt;li&gt;Create configurable mock generators for diverse test scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Not handling errors or edge cases in data generation&lt;/li&gt;
&lt;li&gt;Overlooking resource cleanup in generators (e.g., closing file handles)&lt;/li&gt;
&lt;li&gt;Creating overly complex mock generators that don&apos;t reflect real-world scenarios&lt;/li&gt;
&lt;li&gt;Ignoring performance implications in lazy loading implementations&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Generator pattern proves invaluable for both consuming large datasets efficiently and creating robust test environments for data processing logic. By leveraging Go&apos;s concurrency features, we can create flexible, memory-efficient, and testable data processing pipelines that can handle real-world scenarios and simulated test cases alike.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;While these examples demonstrate the power of the Generator pattern for data processing and testing, real-world implementations may require additional error handling, resource management, and optimizations. Always consider the specific requirements and constraints of your application when applying these patterns.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&amp;lt;div style=&quot;font-size: 0.875rem; color: color-mix(in oklch, var(--text-base) 75%, transparent);&quot;&amp;gt;
&amp;lt;span property=&quot;dct:title&quot;&amp;gt;Educational Go Patterns &amp;lt;/span&amp;gt; by &amp;lt;a rel=&quot;cc:attributionURL dct:creator&quot; property=&quot;cc:attributionName&quot; href=&quot;https://corentings.dev&quot;&amp;gt;Corentin Giaufer Saubert&amp;lt;/a&amp;gt;
is licensed under &amp;lt;a href=&quot;https://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1&quot; target=&quot;_blank&quot; rel=&quot;license noopener noreferrer&quot;&amp;gt;Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International &amp;lt;/a&amp;gt;&amp;lt;br/&amp;gt;
The code examples are licensed under the &amp;lt;a href=&quot;https://opensource.org/licenses/MIT&quot; target=&quot;_blank&quot; rel=&quot;license noopener noreferrer&quot;&amp;gt;MIT License&amp;lt;/a&amp;gt;.
The banner image has been created by (DALL·E) and is licensed under the same license as the article and other graphics.
&amp;lt;/div&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Advanced Generator Pattern in Go: Test Data Generation</title><link>https://corentings.dev/blog/real-world-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/real-world-generator/</guid><description>Practical Generator Pattern examples in Go for test data generation, streaming, and building composable data pipelines.</description><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Advanced Generator Pattern: Test Data for Web Services&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Building upon our previous exploration of the Generator pattern, let&apos;s dive into a more advanced real-world application: generating test data for a web service. This pattern is particularly useful for creating large datasets to stress test APIs or simulate high-load scenarios.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Stress testing web services&lt;/li&gt;
&lt;li&gt;Simulating high-load scenarios for databases&lt;/li&gt;
&lt;li&gt;Creating diverse datasets for QA environments&lt;/li&gt;
&lt;li&gt;Benchmarking system performance&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt;: Easily generate large volumes of test data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customization&lt;/strong&gt;: Tailor data generation to specific test scenarios&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realism&lt;/strong&gt;: Create data that closely mimics production patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: Generate data on-the-fly, reducing storage needs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Define structures representing your API&apos;s data models&lt;/li&gt;
&lt;li&gt;Create generator functions for each data type&lt;/li&gt;
&lt;li&gt;Combine generators to create complex, interrelated data sets&lt;/li&gt;
&lt;li&gt;Use channels to stream generated data to consumers (e.g., API clients)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Advanced Example: E-commerce API Test Data Generator&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Product struct {
    ID    int
    Name  string
    Price float64
}

type Order struct {
    ID       int
    UserID   int
    Products []Product
    Total    float64
}

func productGenerator(count int) &amp;lt;-chan Product {
    out := make(chan Product)
    go func() {
        defer close(out)
        for i := 0; i &amp;lt; count; i++ {
            out &amp;lt;- Product{
                ID:    i + 1,
                Name:  fmt.Sprintf(&quot;Product-%d&quot;, i+1),
                Price: 10.0 + float64(i),
            }
        }
    }()
    return out
}

func orderGenerator(userCount, orderPerUser int, products &amp;lt;-chan Product) &amp;lt;-chan Order {
    out := make(chan Order)
    go func() {
        defer close(out)
        var orderID int
        for userID := 1; userID &amp;lt;= userCount; userID++ {
            for i := 0; i &amp;lt; orderPerUser; i++ {
                orderID++
                var orderProducts []Product
                var total float64
                for j := 0; j &amp;lt; rand.Intn(5)+1; j++ {
                    product := &amp;lt;-products
                    orderProducts = append(orderProducts, product)
                    total += product.Price
                }
                out &amp;lt;- Order{
                    ID:       orderID,
                    UserID:   userID,
                    Products: orderProducts,
                    Total:    total,
                }
            }
        }
    }()
    return out
}

func main() {
    productChan := productGenerator(1000)
    orderChan := orderGenerator(100, 5, productChan)

    // Simulate sending orders to an API
    for order := range orderChan {
        // In a real scenario, you&apos;d send this to your API
        fmt.Printf(&quot;Sending order %d for user %d with total $%.2f\n&quot;, order.ID, order.UserID, order.Total)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example demonstrates a more complex use of the Generator pattern to create realistic test data for an e-commerce API. It generates products and orders, simulating a scenario where multiple users are placing orders with varying numbers of products.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use buffered channels for improved performance when generating large datasets&lt;/li&gt;
&lt;li&gt;Implement cancellation mechanisms for long-running generators&lt;/li&gt;
&lt;li&gt;Consider using worker pools for parallel data generation in complex scenarios&lt;/li&gt;
&lt;li&gt;Seed random number generators for reproducible test data&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generating more data than necessary, leading to increased test times&lt;/li&gt;
&lt;li&gt;Not closing channels properly, causing goroutine leaks&lt;/li&gt;
&lt;li&gt;Overlooking edge cases in data generation, leading to incomplete test coverage&lt;/li&gt;
&lt;li&gt;Generating unrealistic data that doesn&apos;t reflect real-world scenarios&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The advanced application of the Generator pattern for test data generation showcases its power in creating scalable, customizable, and efficient solutions for testing web services. By leveraging Go&apos;s concurrency features, we can create sophisticated data generation pipelines that closely mimic real-world scenarios, enabling thorough and realistic testing of our systems.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article expands on the Generator pattern with a focus on test data generation. While the example provided is more complex, it&apos;s still simplified for educational purposes. In real-world applications, additional considerations such as data variety, error handling, and integration with actual API endpoints would be necessary.&lt;/p&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Mastering the Generator Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-generator/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-generator/</guid><description>Master the Generator Pattern in Go using goroutines and channels. Learn lazy evaluation, composability, and practical examples for data streams and iterators.</description><pubDate>Tue, 03 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Generator Pattern in Go&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Generator pattern in Go is a powerful concurrency pattern used to create functions that produce a sequence of values. It leverages Go&apos;s goroutines and channels to generate data asynchronously, providing an elegant way to work with streams of data or implement iterators.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Creating sequences of numbers or data&lt;/li&gt;
&lt;li&gt;Implementing iterators&lt;/li&gt;
&lt;li&gt;Processing streams of data&lt;/li&gt;
&lt;li&gt;Generating test data&lt;/li&gt;
&lt;li&gt;Simulating real-time data sources&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lazy Evaluation&lt;/strong&gt;: Values are generated on-demand, saving memory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encapsulation&lt;/strong&gt;: Hides the complexity of data generation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concurrency&lt;/strong&gt;: Allows for asynchronous data production&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility&lt;/strong&gt;: Can generate infinite or finite sequences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composability&lt;/strong&gt;: Generators can be chained or combined easily&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;A function creates and returns a channel&lt;/li&gt;
&lt;li&gt;The function starts a goroutine that sends values through the channel&lt;/li&gt;
&lt;li&gt;The caller receives values from the channel, typically using a for-range loop&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func evenGenerator(max int) &amp;lt;-chan int {
    out := make(chan int)
    go func() {
        for i := 0; i &amp;lt;= max; i += 2 {
            out &amp;lt;- i
        }
        close(out)
    }()
    return out
}

func main() {
    for num := range evenGenerator(10) {
        fmt.Println(num)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example creates a generator that produces even numbers up to a specified maximum. The &lt;code&gt;evenGenerator&lt;/code&gt; function returns a receive-only channel (&lt;code&gt;&amp;lt;-chan int&lt;/code&gt;). It starts a goroutine that sends even numbers through the channel and closes it when done.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Line Generator&lt;/h2&gt;
&lt;p&gt;Let&apos;s consider a scenario where we need to generate sample log lines for testing a log analysis system.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogEntry struct {
    Timestamp time.Time
    Level     string
    Message   string
}

func logGenerator(count int) &amp;lt;-chan LogEntry {
    out := make(chan LogEntry)
    go func() {
        levels := []string{&quot;INFO&quot;, &quot;WARNING&quot;, &quot;ERROR&quot;}
        messages := []string{
            &quot;User logged in&quot;,
            &quot;Failed login attempt&quot;,
            &quot;Database connection lost&quot;,
            &quot;API request received&quot;,
            &quot;Cache miss&quot;,
        }
        for i := 0; i &amp;lt; count; i++ {
            out &amp;lt;- LogEntry{
                Timestamp: time.Now().Add(time.Duration(i) * time.Second),
                Level:     levels[rand.Intn(len(levels))],
                Message:   messages[rand.Intn(len(messages))],
            }
            time.Sleep(100 * time.Millisecond) // Simulate delay between log entries
        }
        close(out)
    }()
    return out
}

func main() {
    for entry := range logGenerator(5) {
        fmt.Printf(&quot;[%s] %s: %s\n&quot;, entry.Timestamp.Format(time.RFC3339), entry.Level, entry.Message)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This generator creates a stream of log entries, simulating real-world log generation. It&apos;s useful for testing log processing systems, allowing developers to generate a controlled stream of diverse log entries.&lt;/p&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always close the channel when generation is complete&lt;/li&gt;
&lt;li&gt;Use receive-only channels (&lt;code&gt;&amp;lt;-chan&lt;/code&gt;) as return types&lt;/li&gt;
&lt;li&gt;Consider using context for cancellation in long-running generators&lt;/li&gt;
&lt;li&gt;Implement error handling for robust generators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close channels, leading to goroutine leaks&lt;/li&gt;
&lt;li&gt;Creating infinite generators without proper termination conditions&lt;/li&gt;
&lt;li&gt;Blocking indefinitely on channel operations without timeout mechanisms&lt;/li&gt;
&lt;li&gt;Overusing generators for simple, finite sequences where slices might suffice&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pipeline Pattern: Often used in conjunction with generators to process data streams&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: Can distribute generator output to multiple consumers&lt;/li&gt;
&lt;li&gt;Iterator Pattern: Generators can be seen as concurrent iterators&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Generator pattern in Go provides a powerful way to create sequences or streams of data asynchronously. By leveraging goroutines and channels, it offers lazy evaluation, encapsulation of complex logic, and seamless integration with Go&apos;s concurrency model. Whether you&apos;re working with infinite sequences, simulating data sources, or implementing iterators, the Generator pattern offers a flexible and efficient solution.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article provides an introduction to the Generator pattern in Go. While the pattern is powerful, it&apos;s important to consider the specific needs of your application when implementing it. For production use, additional error handling and optimizations may be necessary.&lt;/p&gt;
&lt;h2&gt;Testing Generators&lt;/h2&gt;
&lt;p&gt;Generators are easy to test when you treat them as contracts: send input, receive output. The goroutine is an implementation detail. I wrote about why that matters in &lt;a href=&quot;/blog/tdd-permission-slip/&quot;&gt;TDD Isn&apos;t About Bugs — It&apos;s Your Permission to Refactor&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Previous:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-producer-consumer/&quot;&gt;Understanding the Producer-Consumer Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;For more advanced concurrency patterns and best practices in Go, stay tuned for future articles! 🚀&lt;/p&gt;
&lt;p&gt;If you want to experiment with the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Producer-Consumer in Go: Beyond the Basics</title><link>https://corentings.dev/blog/real-world-producer-consumer/</link><guid isPermaLink="true">https://corentings.dev/blog/real-world-producer-consumer/</guid><description>Explore advanced aspects of Go&apos;s Producer-Consumer pattern with buffered channels and real-world examples. A beginner-friendly deep dive into practical applications.</description><pubDate>Mon, 02 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Producer-Consumer in Go: Beyond the Basics&lt;/h2&gt;
&lt;p&gt;In our &lt;a href=&quot;/blog/go-pattern-producer-consumer&quot;&gt;previous article&lt;/a&gt;, we explored the fundamentals of the Producer-Consumer pattern in Go. Today, we&apos;ll take it a step further by examining more practical scenarios and introducing buffered channels - a powerful feature that can significantly improve your concurrent applications.&lt;/p&gt;
&lt;h2&gt;Buffered Channels: A Game Changer&lt;/h2&gt;
&lt;p&gt;While regular channels provide immediate synchronization between producers and consumers, sometimes we need more flexibility. Buffered channels act like a small warehouse, temporarily storing items when producers are faster than consumers or when we want to batch process items.&lt;/p&gt;
&lt;p&gt;Let&apos;s see how they work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Unbuffered channel - synchronous
ch := make(chan int)

// Buffered channel - can hold up to 5 items
bufferedCh := make(chan int, 5)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key difference? With a buffered channel, producers can send up to 5 items without waiting for a consumer to receive them. This can significantly improve performance in certain scenarios.&lt;/p&gt;
&lt;h2&gt;Real-World Example: Log Processing System&lt;/h2&gt;
&lt;p&gt;Imagine building a log processing system for a busy web application. Logs come in rapidly during peak hours, but we want to process them in batches for efficiency. This is a perfect use case for buffered channels.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type LogEntry struct {
    Timestamp time.Time
    Level     string
    Message   string
}

func logGenerator(logs chan&amp;lt;- LogEntry) {
    // Simulate incoming logs
    for i := 0; i &amp;lt; 10 ; i++{
        log := LogEntry{
            Timestamp: time.Now(),
            Level:     []string{&quot;INFO&quot;, &quot;WARNING&quot;, &quot;ERROR&quot;}[rand.Intn(3)],
            Message:   fmt.Sprintf(&quot;Event #%d occurred&quot;, i),
        }
        logs &amp;lt;- log
        time.Sleep(100 * time.Millisecond) // Simulate varying log frequencies
    }
    close(logs)
}

func logProcessor(logs &amp;lt;-chan LogEntry) {
    batch := make([]LogEntry, 0, 3) // Process logs in batches of 3

    for log := range logs {
        batch = append(batch, log)

        if len(batch) == 3 {
            // Process batch
            processLogBatch(batch)
            batch = batch[:0] // Clear the batch
        }
    }

    // Process remaining logs
    if len(batch) &amp;gt; 0 {
        processLogBatch(batch)
    }
}

func processLogBatch(batch []LogEntry) {
    fmt.Println(&quot;Processing batch of logs:&quot;)
    for _, log := range batch {
        fmt.Printf(&quot;[%s] %s: %s\n&quot;,
            log.Timestamp.Format(&quot;15:04:05&quot;),
            log.Level,
            log.Message)
    }
    fmt.Println(&quot;Batch processing complete\n&quot;)
}

func main() {
    // Buffer size of 5 to handle burst of logs
    logs := make(chan LogEntry, 5)

    go logGenerator(logs)
    logProcessor(logs)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you run this program, you&apos;ll see output similar to this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Processing batch of logs:
[14:23:45] INFO: Event #0 occurred
[14:23:45] WARNING: Event #1 occurred
[14:23:45] ERROR: Event #2 occurred
Batch processing complete

Processing batch of logs:
[14:23:46] INFO: Event #3 occurred
[14:23:46] WARNING: Event #4 occurred
[14:23:46] INFO: Event #5 occurred
Batch processing complete

Processing batch of logs:
[14:23:47] ERROR: Event #6 occurred
[14:23:47] INFO: Event #7 occurred
[14:23:47] WARNING: Event #8 occurred
Batch processing complete

Processing remaining logs:
[14:23:48] INFO: Event #9 occurred
Batch processing complete
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Understanding the Benefits&lt;/h2&gt;
&lt;p&gt;This implementation showcases several advanced concepts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Burst Handling&lt;/strong&gt;: The buffered channel (size 5) can handle bursts of logs without blocking the producer, even if the consumer is busy processing a batch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Batch Processing&lt;/strong&gt;: Instead of processing each log immediately, we batch them for more efficient processing. This is common in real-world scenarios where batching can reduce database writes or API calls.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Graceful Shutdown&lt;/strong&gt;: The system handles remaining logs before shutting down, ensuring no data is lost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Flexible Processing&lt;/strong&gt;: The batch size (3) is independent of the channel buffer size (5), allowing us to optimize both independently.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;When to Use Buffered Channels&lt;/h2&gt;
&lt;p&gt;Buffered channels are particularly useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your producers and consumers work at different speeds&lt;/li&gt;
&lt;li&gt;You want to batch process items for efficiency&lt;/li&gt;
&lt;li&gt;You need to handle burst of data without blocking producers&lt;/li&gt;
&lt;li&gt;You want to improve performance by reducing synchronization overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, remember that buffered channels don&apos;t solve all problems. They can hide deadlocks and make it harder to reason about your program&apos;s behavior if used incorrectly.&lt;/p&gt;
&lt;h2&gt;Tips for Success&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Choose buffer sizes carefully—too small negates the benefits, too large can hide problems&lt;/li&gt;
&lt;li&gt;Monitor channel capacity in production using &lt;code&gt;len(ch)&lt;/code&gt; and &lt;code&gt;cap(ch)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Consider using contexts for cancellation and timeouts&lt;/li&gt;
&lt;li&gt;Always handle the remaining items when shutting down&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What&apos;s Next?&lt;/h3&gt;
&lt;p&gt;Now that you understand buffered channels and batch processing, you&apos;re ready to explore more advanced patterns like Fan-Out/Fan-In or the Pipeline pattern. Stay tuned for our next article where we&apos;ll dive into those topics!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern becomes even more powerful when combined with buffered channels and batch processing. While staying true to Go&apos;s simplicity, these features allow us to build more efficient and resilient systems.&lt;/p&gt;
&lt;p&gt;Remember: The key to mastering these patterns is practice. Try modifying the log processing example to handle different batch sizes or add error handling. The possibilities are endless!&lt;/p&gt;
</content:encoded></item><item><title>Understanding the Producer-Consumer Pattern in Go</title><link>https://corentings.dev/blog/go-pattern-producer-consumer/</link><guid isPermaLink="true">https://corentings.dev/blog/go-pattern-producer-consumer/</guid><description>Understanding the Producer-Consumer Pattern in Go with channels. Modular architecture, flexible scaling, and real-world concurrent data processing examples.</description><pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Producer-Consumer Pattern in Go&lt;/h2&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern is a fundamental concurrency pattern in Go that elegantly separates the production of data from its consumption. By using channels as intermediaries, this pattern creates a robust foundation for concurrent data processing.&lt;/p&gt;
&lt;h2&gt;When to Use&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;When you need to separate data generation from its processing&lt;/li&gt;
&lt;li&gt;In scenarios where production and consumption rates differ&lt;/li&gt;
&lt;li&gt;When scaling producers and consumers independently is desired&lt;/li&gt;
&lt;li&gt;For managing workload distribution in concurrent systems&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Why Use It&lt;/h2&gt;
&lt;p&gt;This pattern offers several compelling advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modularity&lt;/strong&gt;: Clear separation between data production and consumption logic&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible Scaling&lt;/strong&gt;: Easy to add more producers or consumers as needed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Buffer Management&lt;/strong&gt;: Handles different processing rates naturally&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resource Control&lt;/strong&gt;: Better management of system resources through controlled data flow&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;The pattern consists of three main components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Producers&lt;/strong&gt;: Goroutines that generate data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Queue&lt;/strong&gt;: A channel that acts as a buffer between producers and consumers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumers&lt;/strong&gt;: Goroutines that process the data&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Simple Example&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func producer(ch chan&amp;lt;- int) {
    for i := 0; i &amp;lt; 5; i++ {
        ch &amp;lt;- i
        fmt.Printf(&quot;Produced: %d\n&quot;, i)
    }
    close(ch)
}

func consumer(ch &amp;lt;-chan int) {
    for num := range ch {
        fmt.Printf(&quot;Consumed: %d\n&quot;, num)
    }
}

func main() {
    ch := make(chan int)
    go producer(ch)
    consumer(ch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Real-World Example: Web Scraper&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type Page struct {
    URL     string
    Content string
}

func scraper(urls []string, pages chan&amp;lt;- Page) {
    for _, url := range urls {
        // Simulate web scraping
        time.Sleep(100 * time.Millisecond)
        pages &amp;lt;- Page{
            URL:     url,
            Content: fmt.Sprintf(&quot;Content from %s&quot;, url),
        }
    }
    close(pages)
}

func processor(pages &amp;lt;-chan Page, results chan&amp;lt;- string) {
    for page := range pages {
        // Simulate content processing
        time.Sleep(200 * time.Millisecond)
        results &amp;lt;- fmt.Sprintf(&quot;Processed %s: %s&quot;, page.URL, page.Content)
    }
    close(results)
}

func main() {
    urls := []string{&quot;https://example1.com&quot;, &quot;https://example2.com&quot;}
    pages := make(chan Page)
    results := make(chan string)

    go scraper(urls, pages)
    go processor(pages, results)

    for result := range results {
        fmt.Println(result)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Best Practices and Pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Always use directional channel types (&lt;code&gt;chan&amp;lt;-&lt;/code&gt; for send-only, &lt;code&gt;&amp;lt;-chan&lt;/code&gt; for receive-only)&lt;/li&gt;
&lt;li&gt;Consider buffered channels when producers and consumers work at different speeds&lt;/li&gt;
&lt;li&gt;Implement proper error handling and propagation&lt;/li&gt;
&lt;li&gt;Use context for cancellation when needed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Common Pitfalls:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Forgetting to close channels&lt;/li&gt;
&lt;li&gt;Not handling backpressure when producers are faster than consumers&lt;/li&gt;
&lt;li&gt;Creating deadlocks by improper channel management&lt;/li&gt;
&lt;li&gt;Memory leaks from unclosed goroutines&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Related Patterns&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Pipeline Pattern: For multi-stage data processing&lt;/li&gt;
&lt;li&gt;Worker Pool Pattern: For parallel task processing&lt;/li&gt;
&lt;li&gt;Fan-Out/Fan-In Pattern: For distributed workload processing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The Producer-Consumer pattern is a cornerstone of concurrent programming in Go. Its simplicity and effectiveness make it an excellent starting point for learning concurrency patterns. By separating concerns and managing data flow through channels, it provides a clean and efficient way to handle concurrent data processing tasks.&lt;/p&gt;
&lt;h2&gt;Series Navigation&lt;/h2&gt;
&lt;p&gt;This article is part of the &lt;strong&gt;Go Patterns&lt;/strong&gt; series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href=&quot;/blog/go-pattern-generator/&quot;&gt;Mastering the Generator Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Series:&lt;/strong&gt; &lt;a href=&quot;/tags/go-patterns/&quot;&gt;Go Patterns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;This article is a simple introduction to the Producer-Consumer pattern in Go. For more advanced use cases and optimizations, consider exploring additional resources and best practices in concurrent programming.
I may write more articles on this topic in the future, so stay tuned! 🚀 &amp;lt;br/&amp;gt;
If you want to have a look at the code examples, you can find them on my &lt;a href=&quot;https://github.com/CorentinGS/golang-articles&quot;&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>My Experience at the Karen Asrian Memorial Tournament</title><link>https://corentings.dev/blog/karen-asrian-memorial-tournament/</link><guid isPermaLink="true">https://corentings.dev/blog/karen-asrian-memorial-tournament/</guid><description>My experience at the Karen Asrian Memorial chess tournament in Armenia—games, culture, and lessons learned.</description><pubDate>Fri, 17 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;My recent trip to Armenia wasn&apos;t just a vacation—it was a chess pilgrimage!
I participated in the prestigious Karen Asrian Memorial, a chess tournament that pushed me to my limits and left me with unforgettable memories.&lt;/p&gt;
&lt;h2&gt;The Tournament&lt;/h2&gt;
&lt;p&gt;The Karen Asrian Memorial is a tournament dedicated to a chess legend.
GM Avetik Grigoryan wrote a fantastic article about him, which I highly advise you to read:
&amp;lt;a rel=&apos;noopener noreferrer&apos; aria-labal=&quot;In Memory of GM Karen Asrian&quot; href=&quot;https://chessmood.com/blog/karen-asrian&quot;&amp;gt;In Memory of GM Karen Asrian&amp;lt;/a&amp;gt;.
One of the tournament&apos;s highlights was meeting Levon Aronian, a true chess superstar. I was fortunate to meet such a player in person.&lt;/p&gt;
&lt;p&gt;The tournament format consisted of nine rounds spread over nine days. Each game was a marathon, lasting between 3 and 4.5 hours.&lt;/p&gt;
&lt;h2&gt;My performance&lt;/h2&gt;
&lt;p&gt;Overall, this was my most robust performance yet.
I beat a player rated over 2150 and even faced a Grandmaster in a classical chess game for the first time.
While I achieved a dominant position, time pressure ultimately led to a bittersweet loss.&lt;/p&gt;
&lt;p&gt;At the beginning of the tournament, I was rated 1803 Elo and was the 67th player over 73 according to the initial ranking.
I finished with 3.5 points over 9, half a point more than my initial goal. I earned 34 fide elo points and a performance of 2000 elo!
My final rank is 57th, meaning I had an excellent performance.&lt;/p&gt;
&lt;h2&gt;Chessmood&lt;/h2&gt;
&lt;p&gt;Speaking of improvement, a big part of my chess journey wouldn&apos;t have been possible without the Chessmood team. Their training platform helped me climb over 300 points in online blitz and rapid chess ratings.
This translated into real-world results, allowing me to consistently defeat players rated around 2000 Elo.
I also had the opportunity to meet them in person. I played in the tournament with IM David Shahinyan, who helped me improve recently.
Furthermore, I could visit Chessmood&apos;s office, discover their waiting room, and have a great barbecue with them!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This Armenian adventure tested my skills and reminded me of the power of dedicated practice.
It left me eager to return and continue my chess journey!&lt;/p&gt;
&lt;p&gt;If you want to know more about what I discovered in Armenia besides chess, stay tuned for my next article! I would like to share some insights about the country and its culture.&lt;/p&gt;
</content:encoded></item><item><title>Solving the Sum of Squares Problem: Optimizing Performance</title><link>https://corentings.dev/blog/optimizing-goroutines-sum-of-squares/</link><guid isPermaLink="true">https://corentings.dev/blog/optimizing-goroutines-sum-of-squares/</guid><description>Optimize Go performance by solving the sum of squares problem. Benchmark goroutines vs sequential code and avoid common concurrency pitfalls.</description><pubDate>Wed, 09 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Disclaimer: Enhancing Algorithm Discussions&lt;/h2&gt;
&lt;p&gt;Before delving into the main topic, I want to express my respect for the original post&apos;s intent.
My response aims to provide slight corrections that can benefit readers seeking accurate information.
In the spirit of shared learning, I intend to contribute constructively rather than criticize. &amp;lt;br /&amp;gt;Clarifications and alternative viewpoints can foster a deeper understanding of complex concepts. Let&apos;s continue engaging in open discussions, embracing diverse insights as we collectively refine our knowledge.&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Greetings, fellow Gophers! Are you ready to explore the intriguing world of Goroutines and unravel the mysteries of the sum of squares problem?
Recently, a friend learning Go shared a &amp;lt;a href={&apos;https://medium.com/@ShivamSouravJha/golang-only-things-i-know-for-the-interview-4322d29d67a3&apos;} style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;medium article&amp;lt;/a&amp;gt; with me.
It discussed Goroutines and how they can be used to solve a simple problem: calculating the sum of square numbers in a given array.&lt;/p&gt;
&lt;p&gt;In this blog post, we&apos;ll delve into how Goroutines, channels, and data structures in Golang can be used to tackle the sum of squares problem. We&apos;ll also discuss common pitfalls and inefficient practices that can hinder performance.
Fear not, my friends, for we shall also unveil secrets to optimizing your code and achieving exceptional performance. So, let&apos;s embark on this quest together!&lt;/p&gt;
&lt;h2&gt;The Sum of Squares Problem: A simple problem&lt;/h2&gt;
&lt;p&gt;The medium article of my friend addressed the following problem: &quot;6. Write a program that returns the sum of the squares of each element of an array in Golang.&quot;&lt;/p&gt;
&lt;p&gt;Easy enough. We need to iterate through every element in the array and add the square of each one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func simpleSumSquare(items []int) int {
	total := 0 // total sum
	for i := 0; i &amp;lt; len(items); i++ {
		total += items[i] * items[i] // square the item and add it to the total
	}
	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can we simplify it further? The code is reasonably straightforward. However, can we make it faster?&lt;/p&gt;
&lt;h2&gt;Goroutines: Harnessing the Power of Concurrency&lt;/h2&gt;
&lt;p&gt;Goroutines, the superheroes of Golang, are lightweight threads that enable concurrent task execution.
By using Goroutines, we can execute multiple functions simultaneously, making our code more efficient and faster. But with great power comes great responsibility.&amp;lt;br /&amp;gt;
One common mistake in solving the sum of squares problem is the improper use of Goroutines.
Some developers create too many Goroutines without proper synchronization, leading to chaos and incorrect results.
Remember, coordination is crucial!&lt;/p&gt;
&lt;h2&gt;Goroutines: Common pitfall&lt;/h2&gt;
&lt;p&gt;When solving the sum of squares problem, the choice of algorithm and data structures significantly impacts performance.
The aforementioned medium blog post highlights the use of Goroutines and their potential to substantially improve performance.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sumSquare(items []int) int {
	number := make(chan int)   // channel for sending numbers
	response := make(chan int) // channel for receiving responses

	var wg sync.WaitGroup // wait group for waiting for all goroutines to finish

	total := 0 // total sum

	// Create a goroutine for each item in the slice
	for _, item := range items {
		wg.Add(1)           // increment the wait group counter
		go func(item int) { // create a goroutine
			defer wg.Done()    // decrement the wait group counter when the goroutine finishes
			sum1 := &amp;lt;-number   // receive a number from the number channel
			sum1 = sum1 * sum1 // square the number
			response &amp;lt;- sum1   // send the result to the response channel
		}(item) // pass the item to the goroutine
		number &amp;lt;- item      // send the item to the number channel
		total += &amp;lt;-response // receive the result from the response channel
	}

	defer close(number)   // close the number channel
	defer close(response) // close the response channel

	wg.Wait() // wait for all goroutines to finish

	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This code creates a new goroutine for each item in the array and then performs the squaring operation. It uses channels to pass values between the goroutine and the main thread.&lt;/p&gt;
&lt;p&gt;However, is this code correct? As discussed in &amp;lt;a href={&apos;../mergesort-parallel&apos;} style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;prefetch noopener&quot;&amp;gt;my previous blog post about the mergesort algorithm&amp;lt;/a&amp;gt;, there are better practices than spawning numerous goroutines.
More than being lightweight is needed to justify using many goroutines; it still uses memory and time, and we must be cautious.&lt;/p&gt;
&lt;p&gt;As always, let&apos;s benchmark the code!&lt;/p&gt;
&lt;h2&gt;Benchmarking: The Quest for Ultimate Performance&lt;/h2&gt;
&lt;p&gt;Benchmarking lets us measure code performance and compare implementations to identify the best.&lt;/p&gt;
&lt;p&gt;With Golang&apos;s built-in benchmarking tools, we can easily measure the execution time of our code and identify bottlenecks. By tweaking our implementation and experimenting with different approaches, we can achieve optimal performance and revel in our achievements.&lt;/p&gt;
&lt;p&gt;Hence, I&apos;ve created a small function to benchmark our algorithms using randomly generated arrays of varying sizes to observe their performance at different scales:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func benchmarkFramework(b *testing.B, sumFunction func([]int) int) {
	sizes := [][]int{RandomArray(100, 0, 100),
		RandomArray(1000, 0, 1000),
		RandomArray(10000, 0, 10000),
		RandomArray(100000, 0, 100000),
		RandomArray(1000000, 0, 1000000),
	}
	b.ResetTimer()
	for _, size := range sizes {
		b.Run(fmt.Sprintf(&quot;%d&quot;, len(size)), func(b *testing.B) {
			for i := 0; i &amp;lt; b.N; i++ {
				sumFunction(size)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are the results:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;time/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100000&lt;/td&gt;
&lt;td&gt;43.8µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000000&lt;/td&gt;
&lt;td&gt;437µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100&lt;/td&gt;
&lt;td&gt;75.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000&lt;/td&gt;
&lt;td&gt;730µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/10000&lt;/td&gt;
&lt;td&gt;7.24ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100000&lt;/td&gt;
&lt;td&gt;72.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000000&lt;/td&gt;
&lt;td&gt;739ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As we can see, the simple algorithm outperforms the complex one! Why? Because spawning too many Goroutines slows down the program and consumes a significant amount of memory.&lt;/p&gt;
&lt;h2&gt;Data Structures &amp;amp; Algorithms: Choose Wisely&lt;/h2&gt;
&lt;p&gt;When solving the sum of squares problem, the choice of data structures and algorithms significantly impacts performance.&lt;/p&gt;
&lt;p&gt;I decided to develop a more efficient function. Consequently, I used chunks and a Goroutine pool.&lt;/p&gt;
&lt;p&gt;A goroutines pool manages a set number of reusable goroutines, reducing overhead in concurrent programs. Chunks break data into segments for parallel processing, optimizing resource use, and enhancing efficiency. Combining these techniques streamlines parallelism, maximizing concurrency benefits.&lt;/p&gt;
&lt;p&gt;Here is my code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func simpleParallelSumSquare(items []int) int {
	const chunkSize = 10000

	if len(items) &amp;lt;= 10000 { // Threshold for small slices
		return simpleSumSquare(items) // Use the simpleSumSquare function
	}

	// Divide the items into chunks
	chunks := make([][]int, 0)
	for i := 0; i &amp;lt; len(items); i += chunkSize {
		end := i + chunkSize // end index for the chunk
		if end &amp;gt; len(items) {
			end = len(items) // last chunk may be smaller than chunkSize
		}
		chunks = append(chunks, items[i:end]) // append the chunk to the chunks slice
	}

	// Create a goroutine for each chunk
	wg := sync.WaitGroup{}
	resultChan := make(chan int, len(chunks)) // channel for receiving results

	for _, chunk := range chunks { // iterate over the chunks
		wg.Add(1)              // increment the wait group counter
		go func(chunk []int) { // create a goroutine
			resultChan &amp;lt;- simpleSumSquare(chunk) // send the result to the result channel
			wg.Done()                            // decrement the wait group counter when the goroutine finishes
		}(chunk) // pass the chunk to the goroutine
	}

	wg.Wait()         // Wait for all goroutines to finish
	close(resultChan) // close the result channel

	// Sum the results
	total := 0
	for partialSum := range resultChan {
		total += partialSum
	}

	return total // return the total sum
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Can this code be improved further? I could reduce the number of allocations and enhance performance. However, I opted for simplicity to better illustrate pitfalls and good practices related to Goroutines.&lt;/p&gt;
&lt;p&gt;Is it genuinely efficient? Let&apos;s benchmark it, as always!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;time/op&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/100000&lt;/td&gt;
&lt;td&gt;43.8µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleSumSquare/1000000&lt;/td&gt;
&lt;td&gt;437µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100&lt;/td&gt;
&lt;td&gt;75.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000&lt;/td&gt;
&lt;td&gt;730µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/10000&lt;/td&gt;
&lt;td&gt;7.24ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/100000&lt;/td&gt;
&lt;td&gt;72.4ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SumSquare/1000000&lt;/td&gt;
&lt;td&gt;739ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/100&lt;/td&gt;
&lt;td&gt;49.5ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/1000&lt;/td&gt;
&lt;td&gt;450ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/10000&lt;/td&gt;
&lt;td&gt;4.41µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/100000&lt;/td&gt;
&lt;td&gt;18.0µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SimpleParallelSumSquare/1000000&lt;/td&gt;
&lt;td&gt;67.2µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As we can observe, our new code is significantly faster!&lt;/p&gt;
&lt;h2&gt;Pitfalls and Bad Practices: The Road to Destruction&lt;/h2&gt;
&lt;p&gt;Ah, the treacherous road of pitfalls and bad practices. One common mistake is the excessive creation and destruction of Goroutines.
Creating Goroutines carries a cost; making too many can slow your program and consume unnecessary resources.&lt;/p&gt;
&lt;p&gt;Furthermore, we can use unsafe features to improve our code&apos;s performance but must not sacrifice maintainability and safety for speed if it&apos;s not really required.
If you want to learn more about unsafe features available on Golang, you can look at the code published &amp;lt;a href=&apos;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_sum_square&apos; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&amp;gt; here on my Github &amp;lt;/a&amp;gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion: The Sum of Success&lt;/h2&gt;
&lt;p&gt;Congratulations, my friends! We&apos;ve journeyed through Goroutines, channels, and data structures, conquering the sum of squares problem.
We&apos;ve learned from our mistakes, optimized our code, and achieved top-notch performance.&lt;/p&gt;
&lt;p&gt;But remember, the pursuit of knowledge is everlasting.
Keep exploring, experimenting, and pushing the boundaries of what&apos;s possible. And always remember, with great power comes great responsibility.
So go forth, my fellow Gophers, may your code be swift, your bugs be few, and your adventures be legendary!&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/simple-go-vs-goroutines/&quot;&gt;Goroutine vs Simple function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://medium.com/@ShivamSouravJha/golang-only-things-i-know-for-the-interview-4322d29d67a3&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener nofollow&quot;&amp;gt;Medium article&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_sum_square&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Code snippets&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Upgrading to dnf5: A step-by-step guide for Fedora users</title><link>https://corentings.dev/blog/dnf5-step-by-step/</link><guid isPermaLink="true">https://corentings.dev/blog/dnf5-step-by-step/</guid><description>Step-by-step guide to upgrading from DNF to DNF5 on Fedora. Faster package management with C++ multi-threaded performance.</description><pubDate>Fri, 28 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Upgrading to dnf5: A guide for Fedora users&lt;/h2&gt;
&lt;p&gt;:::warning
This guide is old and may contain outdated information.
:::&lt;/p&gt;
&lt;h2&gt;Introduction&lt;/h2&gt;
&lt;p&gt;DNF5 is Fedora&apos;s new, faster, and more powerful package manager. Although it is still in development and won&apos;t be the default package manager until Fedora 39, you can install it now and start using it.
This blog post will show you how to replace DNF with DNF5 on your Fedora system.&lt;/p&gt;
&lt;h2&gt;What&apos;s the difference between dnf5 and dnf ?&lt;/h2&gt;
&lt;p&gt;DNF is an old, single-threaded package manager with much legacy code. It&apos;s written in Python and is usually described as slow by users.
DNF5, on the other hand, is a complete rewrite of DNF written in C++. It&apos;s multi-threaded, has a better user experience, should be easier to maintain, and is faster.&lt;/p&gt;
&lt;h2&gt;Why should I upgrade to dnf5?&lt;/h2&gt;
&lt;p&gt;Upgrading to DNF5 offers several benefits, including improved speed and efficiency.
DNF5 is designed to be faster and more efficient than DNF, which can help speed up your system&apos;s package installation and update process.
Additionally, as it will be the default package manager in &lt;a href=&quot;https://github.com/rpm-software-management/dnf5/issues/411&quot;&gt;Fedora39&lt;/a&gt;,
starting to use it now and reporting any bugs you encounter will help the developers fix them before the release.&lt;/p&gt;
&lt;h2&gt;How to upgrade to dnf5?&lt;/h2&gt;
&lt;h3&gt;&lt;strong&gt;Step 1: Install dnf5&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;To install DNF5 from the unstable repository, run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dnf copr enable rpmsoftwaremanagement/dnf5-unstable ;
dnf install dnf5 dnf5-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you&apos;re using sudo, use this command instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf copr enable rpmsoftwaremanagement/dnf5-unstable ;
sudo dnf install dnf5 dnf5-plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;Step 2: Create an alias for dnf5 (optional)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;You can create an alias if you want to use DNF5 instead of DNF. Run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias dnf=&quot;dnf5&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To make this alias permanent, add it to your &lt;code&gt;~/.bashrc&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;alias dnf=\&quot;dnf5\&quot;&quot; &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or add it to your &lt;code&gt;~/.zshrc&lt;/code&gt; file if you use zsh:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;alias dnf=\&quot;dnf5\&quot;&quot; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;Remember that DNF5 is still in development and not ready for production use. It may contain bugs and should not be used on production systems. Use it at your own risk. I am not responsible for any damage caused by using DNF5.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;DNF5 may still have some bugs, so it&apos;s essential to experiment with it and keep this blog post up-to-date with the latest changes.
This guide helped you upgrade to DNF5. If you have any questions or suggestions, feel free to contact me on &amp;lt;a href=&quot;https://twitter.com/GSCorentinDev&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;Twitter&amp;lt;/a&amp;gt; or &amp;lt;a href=&quot;https://corentings.dev/discord&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;Discord&amp;lt;/a&amp;gt;.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://fedoraproject.org/wiki/Changes/ReplaceDnfWithDnf5&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Fedora Project&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://www.reddit.com/r/Fedora/comments/12jv7uc/what_is_the_state_of_affairs_with_fedora_38_and/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;r/fedora&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;a href=&quot;https://github.com/rpm-software-management/dnf5/issues/411&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&amp;gt;Github issue&amp;lt;/a&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Merge Sort using Goroutines</title><link>https://corentings.dev/blog/mergesort-parallel/</link><guid isPermaLink="true">https://corentings.dev/blog/mergesort-parallel/</guid><description>Implement parallel Merge Sort in Go using goroutines. Compare performance with sequential version and learn when parallelization pays off.</description><pubDate>Wed, 11 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Parallel Merge Sort vs Simple Merge Sort&lt;/h2&gt;
&lt;p&gt;This is a simple example of how to use goroutines to &lt;strong&gt;parallelize&lt;/strong&gt; a merge sort algorithm.
We compare the performance of a simple merge sort algorithm with a parallel merge sort algorithm that uses goroutines.&lt;/p&gt;
&lt;h2&gt;The Merge Sort Algorithm&lt;/h2&gt;
&lt;p&gt;The merge sort algorithm is a divide and conquer algorithm that recursively splits the input array into two halves,
sorts each half, and then merges the two sorted halves into a single sorted array.&lt;/p&gt;
&lt;p&gt;To speed up the merge sort algorithm, we use insertion sort for small subarrays (less than 12 elements).&lt;/p&gt;
&lt;p&gt;The implementation of the algorithm uses generics to allow sorting of any type of numbers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func MergeSort[T Number](items []T) []T {
	size := len(items)
	if size &amp;lt; 2 {
		return items
	}

	if size &amp;lt; K {
		return Insertionsort(items)
	}

	middle := size / 2
	var a = MergeSort(items[:middle])
	var b = MergeSort(items[middle:])

	return merge(a, b)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Merge Sort Algorithm with Goroutines&lt;/h2&gt;
&lt;p&gt;The parallel merge sort algorithm uses &lt;strong&gt;goroutines&lt;/strong&gt; to sort the two halves of the input array in parallel.&lt;/p&gt;
&lt;p&gt;To prevent the creation of too many goroutines, we use a &lt;strong&gt;threshold&lt;/strong&gt; to determine when to use goroutines.
If the size of the input array is less than the threshold,
we use a simple merge sort algorithm instead of a parallel one.&lt;/p&gt;
&lt;p&gt;Here we use a threshold of 512 elements. We can benchmark the performance of the algorithm with
different thresholds to find the optimal threshold, but we will not do that in this example.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ParallelMerge Perform merge sort on a slice using goroutines
func ParallelMerge[T Number](items []T) []T {
	if len(items) &amp;lt; 2 {
		return items
	}

	// Use a simple merge sort algorithm if the size of the input array is less than the threshold
	if len(items) &amp;lt; 512 {
		return MergeSort(items)
	}

	// Create the wait group to wait for the goroutines to finish
	var wg sync.WaitGroup
	wg.Add(1)

	var middle = len(items) / 2  // Find the middle index of the input array
	var a []T                   // Create a slice to hold the first half of the input array
	go func() {                // Create a goroutine to sort the first half of the input array
		defer wg.Done()       // Decrement the wait group counter when the goroutine finishes
		a = ParallelMerge(items[:middle]) // Sort the first half of the input array
	}()
	var b = ParallelMerge(items[middle:]) // Sort the second half of the input array

	wg.Wait() // Wait for the goroutine to finish
	return merge(a, b) // Merge the two sorted halves
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Benchmarking the Merge Sort Algorithms&lt;/h2&gt;
&lt;p&gt;Now we can benchmark our merge sort algorithms to compare their &lt;strong&gt;performance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;To benchmark the algorithms, we use arrays of different sizes and &lt;strong&gt;measure the time&lt;/strong&gt; it takes to sort the arrays.&lt;/p&gt;
&lt;p&gt;To run the benchmark we use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -bench=. -benchtime 5s &amp;gt; benchmark.txt &amp;amp;&amp;amp; benchstat benchmark.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benchstat is a tool that can be used to compare the results of benchmarks.&lt;/p&gt;
&lt;p&gt;The results of the benchmark are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name                                   time/op
Mergesort/1000                     14.6µs ± 0%
Mergesort/10000                     473µs ± 0%
Mergesort/100000                   6.33ms ± 0%
Mergesort/1000000                  87.4ms ± 0%

MergesortWithGoroutines/1000       18.7µs ± 0%
MergesortWithGoroutines/10000       217µs ± 0%
MergesortWithGoroutines/100000     2.71ms ± 0%
MergesortWithGoroutines/1000000    29.0ms ± 0%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As we can see, the parallel merge sort algorithm is &lt;strong&gt;much faster&lt;/strong&gt; than the simple merge sort algorithm for &lt;strong&gt;large arrays&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why is the Parallel Merge Sort Algorithm Faster?&lt;/h2&gt;
&lt;p&gt;The parallel merge sort algorithm is faster because it uses goroutines to sort the two halves of the input array in &lt;strong&gt;parallel&lt;/strong&gt;.
The simple merge sort algorithm sorts the two halves of the input array &lt;strong&gt;sequentially&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As discussed in the previous article, the cost of &lt;strong&gt;creating a goroutine can be high&lt;/strong&gt;.
So we should use a parallel merge sort algorithm only when the size of the input array is &lt;strong&gt;large enough to justify the cost&lt;/strong&gt; of creating goroutines.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this example, we have seen how to use &lt;strong&gt;goroutines to parallelize&lt;/strong&gt; a merge sort algorithm. We have also benchmarked the performance of the merge sort algorithms to compare their performance.
Goroutines are a &lt;strong&gt;powerful tool&lt;/strong&gt; that should be used &lt;strong&gt;only when the cost of creating goroutines is justified&lt;/strong&gt; by the performance improvement.&lt;/p&gt;
&lt;p&gt;In this example, the code isn&apos;t more complex when using goroutines therefore it&apos;s worth using them.&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/go-pattern-worker/&quot;&gt;Mastering the Worker Pool Pattern in Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;The code for this article can be found on &amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_merge_sort&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt; GitHub &amp;lt;/a&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>Goroutine vs Simple function</title><link>https://corentings.dev/blog/simple-go-vs-goroutines/</link><guid isPermaLink="true">https://corentings.dev/blog/simple-go-vs-goroutines/</guid><description>When are goroutines overkill in Go? Benchmark comparison showing simple functions can outperform goroutines for small datasets.</description><pubDate>Wed, 11 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Goroutine vs Simple function&lt;/h2&gt;
&lt;p&gt;This is a simple example of why goroutines might be &lt;strong&gt;overkill&lt;/strong&gt; for some tasks and less efficient than a simple function.&lt;/p&gt;
&lt;h2&gt;Structures&lt;/h2&gt;
&lt;p&gt;We got a simple structure that contains sensitive information that we don&apos;t want to be exposed to the outside world.
Therefore, we created a second structure that hides the sensitive information and only exposes the information we want to be public.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Pineapple is a struct that represents a database object with sensitive data that should be hidden
type Pineapple struct {
	Paro       string `faker:&quot;name&quot;`
	Turkey     string `faker:&quot;name&quot;`
	Banana     string `faker:&quot;name&quot;`
	Age        int    `faker:&quot;number&quot;`
	Size       int    `faker:&quot;number&quot;`
	IsAlive    bool
	ID         uint
	SecretCode []byte
	Created    time.Time
	Updated    time.Time
}

// SafePineApple is a struct that represents a Pineapple object without sensitive data
type SafePineApple struct {
    Paro    string
    Turkey  string
    Banana  string
    IsAlive bool
    Age     int
    ID      uint
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conversion&lt;/h2&gt;
&lt;p&gt;We need to convert our Pineapple object to a SafePineApple object.
We can do this by creating a method on the Pineapple struct that returns a SafePineApple object.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ToSafePineApple converts a Pineapple object to a SafePineApple object
func (p *Pineapple) ToSafePineApple() SafePineApple {
	return SafePineApple{
		Paro:    p.Paro,
		Turkey:  p.Turkey,
		Banana:  p.Banana,
		IsAlive: p.IsAlive,
		Age:     p.Age,
		ID:      p.ID,
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Use case&lt;/h2&gt;
&lt;p&gt;In our use case we have an &lt;strong&gt;array of Pineapple objects&lt;/strong&gt; coming from our database that we want to convert to &lt;strong&gt;SafePineApple objects&lt;/strong&gt; and store them in a &lt;strong&gt;new array&lt;/strong&gt;.
The &lt;strong&gt;order of the objects&lt;/strong&gt; in the array should be the same as the original array as it has already been sorted by a sql query.&lt;/p&gt;
&lt;h2&gt;Simple function&lt;/h2&gt;
&lt;p&gt;We can do this by creating a simple function that takes an array of Pineapple objects and returns an array of SafePineApple objects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SimpleConvertPineApplesToSafety converts an array of Pineapple objects to an array of SafePineApple objects
func SimpleConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	safePineApples := make([]SafePineApple, len(pineapples))

	for idx, pineapple := range pineapples {
		safePineApples[idx] = pineapple.ToSafePineApple()
	}

	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is very &lt;strong&gt;simple&lt;/strong&gt; and &lt;strong&gt;easy to understand&lt;/strong&gt;. We loop through the array of Pineapple objects and convert them to SafePineApple objects.&lt;/p&gt;
&lt;h2&gt;Goroutine without mutex&lt;/h2&gt;
&lt;p&gt;We can do this by using &lt;strong&gt;goroutines&lt;/strong&gt; to work on the array concurrently and store the results in a new array.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func GoroutinesNoMutexConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	// Create a slice to store the SafePineApples
	safePineApples := make([]SafePineApple, len(pineapples)/2, len(pineapples))
	safePineApples2 := make([]SafePineApple, len(pineapples)/2)

	var wg sync.WaitGroup // Create a WaitGroup to wait for all goroutines to finish
	wg.Add(1)            // Add 1 to the WaitGroup

	// Create a goroutine to convert the first half of the Pineapple objects
	go func(chunk []Pineapple) {
		defer wg.Done() // Decrement the WaitGroup when the goroutine is done
		for idx, pineapple := range chunk { // Loop through the chunk of Pineapple objects
			safePineApples[idx] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
		}
	}(pineapples[:len(pineapples)/2]) // Pass the first half of the Pineapple objects to the goroutine

	// Convert the second half of the Pineapple objects in the main thread
	for idx, pineapple := range pineapples[len(pineapples)/2:] {
		safePineApples2[idx] = pineapple.ToSafePineApple()
	}

	// Wait for all goroutines to finish
	wg.Wait()

	// Group both pineapples
	safePineApples = append(safePineApples, safePineApples2...)

	// Return the SafePineApples
	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function is a bit more &lt;strong&gt;complex&lt;/strong&gt; than the simple function. We use a goroutine to convert the first half while the other half is handled by the main thread.
We use a &lt;strong&gt;WaitGroup&lt;/strong&gt; to wait for the goroutine to finish before returning the results.&lt;/p&gt;
&lt;h2&gt;Goroutine with mutex&lt;/h2&gt;
&lt;p&gt;We can also add &lt;strong&gt;mutexes&lt;/strong&gt; to the goroutine to make it &lt;strong&gt;thread safe&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func GoroutinesConvertPineApplesToSafety(pineapples []Pineapple) []SafePineApple {
	// Create a slice to store the SafePineApples
	safePineApples := make([]SafePineApple, len(pineapples))

	// Split the offers into chunks
	chunks := [][]Pineapple{pineapples[:len(pineapples)/2], pineapples[len(pineapples)/2:]}

	mutex := sync.Mutex{} // Create a mutex to lock the slice when writing to it

	var wg sync.WaitGroup // Create a WaitGroup to wait for all goroutines to finish
	wg.Add(1)           // Add 1 to the WaitGroup

	// Create a goroutine to convert the first half of the Pineapple objects
	go func(chunk []Pineapple) {
		defer wg.Done() // Decrement the WaitGroup when the goroutine is done
		for idx, pineapple := range chunk { // Loop through the chunk of Pineapple objects
			mutex.Lock() // Lock the mutex
			safePineApples[idx] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
			mutex.Unlock()  // Unlock the mutex
		}
	}(chunks[0]) // Pass the first half of the Pineapple objects to the goroutine

	// Convert the second half of the Pineapple objects in the main thread
	for idx, pineapple := range chunks[1] {
		mutex.Lock() // Lock the mutex
		safePineApples[idx+len(chunks[0])] = pineapple.ToSafePineApple() // Convert the Pineapple object to a SafePineApple object
		mutex.Unlock() // Unlock the mutex
	}

	// Wait for all goroutines to finish
	wg.Wait()

	return safePineApples
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function make use of the mutex to lock the slice when writing to it.
This makes sure that the goroutine and the main thread don&apos;t write to the same index at the same time but instead wait for the other to finish.&lt;/p&gt;
&lt;h2&gt;Benchmark&lt;/h2&gt;
&lt;p&gt;Now that we have our functions we can benchmark them to see which one is the &lt;strong&gt;fastest.&lt;/strong&gt;
To benchmark our functions we run them with arrays of different sizes.&lt;/p&gt;
&lt;p&gt;Our benchmark function looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func Benchmark_SimpleConvertPineApplesToSafety(b *testing.B) {
	for _, n := range []int{500, 1000, 2000, 5000, 10000} {
		b.Run(fmt.Sprintf(&quot;Benchmark_SimpleConvertPineApplesToSafety-%d&quot;, n), func(b *testing.B) {
			pineApples := make([]Pineapple, n)
			var pine Pineapple
			for i := 0; i &amp;lt; n; i++ {
				_ = faker.FakeData(&amp;amp;pine)
				pine.Created = time.Now().AddDate(0, 0, -i)
				pine.ID = uint(i)
				pine.IsAlive = true
				pineApples[i] = pine
			}
			for i := 0; i &amp;lt; b.N; i++ {
				SimpleConvertPineApplesToSafety(pineApples)
			}
		})
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To run the benchmark we use the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -bench=. -benchtime 5s &amp;gt; benchmark.txt &amp;amp;&amp;amp; benchstat benchmark.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Benchstat is a tool that can be used to compare the results of benchmarks.&lt;/p&gt;
&lt;p&gt;The results of the benchmark are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name                                                                        time/op
Benchmark_SimpleConvertPineApplesToSafety-500-32                          15.8µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-1000-32                         32.0µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-2000-32                         66.5µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-5000-32                          193µs ± 0%
Benchmark_SimpleConvertPineApplesToSafety-10000-32                         465µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-500-32                      23.5µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-1000-32                     46.2µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-2000-32                     87.7µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-5000-32                      242µs ± 0%
Benchmark_GoroutinesConvertPineApplesToSafety-10000-32                     507µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-500-32               28.3µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-1000-32              48.7µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-2000-32               105µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-5000-32               257µs ± 0%
Benchmark_NoMutexGoroutinesConvertPineApplesToSafety-10000-32              533µs ± 0%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see the &lt;strong&gt;simple function is the fastest&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Why ?&lt;/h2&gt;
&lt;p&gt;The reason why the simple function is the fastest is that the process of converting the Pineapple objects to SafePineApple objects is very fast.
The time it takes to create the goroutines and wait for them to finish is longer than the time it takes to convert the Pineapple objects to SafePineApple objects.&lt;/p&gt;
&lt;p&gt;Furthermore, in our goroutines implementation we have to convert the Pineapple objects then lock the mutex, write to the slice and unlock the mutex.
This is a lot of &lt;strong&gt;overhead for a very simple task&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Don&apos;t use goroutines when you don&apos;t need them.&lt;/em&gt; That may seem obvious, but it&apos;s easy to forget when you&apos;re trying to optimize your code.
In this example the overhead of creating the goroutines and waiting for them to finish is &lt;strong&gt;longer&lt;/strong&gt; than the time it takes to convert the Pineapple objects to SafePineApple objects.&lt;/p&gt;
&lt;p&gt;Goroutines are great for tasks that take a &lt;strong&gt;long time to complete&lt;/strong&gt; and can be done in parallel.
I would suggest to write the &lt;strong&gt;simplest code possible&lt;/strong&gt; and then benchmark it to see if you can improve it using goroutines instead.&lt;/p&gt;
&lt;p&gt;Moreover, simple code is &lt;strong&gt;easier to read and maintain&lt;/strong&gt; than complex code, that&apos;s why writing complex code might not be necessary if performance is not an issue.
&lt;strong&gt;I&apos;d prefer to have a simple function that takes a few milliseconds longer to complete than a complex function.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Code&lt;/h2&gt;
&lt;p&gt;The code for this article can be found on &amp;lt;a href=&quot;https://github.com/CorentinGS/go-teaching/tree/main/goroutines_simple_vs_complex&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt; GitHub &amp;lt;/a&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>How to optimize a Go deployment with Docker </title><link>https://corentings.dev/blog/docker-and-go/</link><guid isPermaLink="true">https://corentings.dev/blog/docker-and-go/</guid><description>Optimize your Go deployment with Docker using multi-stage builds. Reduce image size from 1GB to 15MB with practical Dockerfile examples.</description><pubDate>Tue, 08 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;How to optimize a Go deployment with Docker ?&lt;/h2&gt;
&lt;p&gt;In this article, I will present the different steps that led me to optimize the deployment of services in &lt;strong&gt;golang&lt;/strong&gt;
using &lt;strong&gt;docker&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;A simple Dockerfile&lt;/h2&gt;
&lt;p&gt;When I started Go and deployed a rest api service for my Memnix application, I used a basic Dockerfile
that I had found on the internet.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19
RUN mkdir -p /go/src/myapp
WORKDIR /go/src/myapp
COPY archived /go/src/myapp

RUN go get -d -v
RUN go install -v

EXPOSE 8080
CMD [&quot;/go/bin/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem is that I ended up with a 2GB image while my project compiled locally without docker and optimized was only 10MB!
So I decided to try to optimize the size of my Docker image as much as possible.&lt;/p&gt;
&lt;h2&gt;The first step: using a multi-stage build&lt;/h2&gt;
&lt;p&gt;It is possible to separate our Dockerfile in several parts and to use several images:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One image to build the project&lt;/li&gt;
&lt;li&gt;A minimalist image to deploy it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This method allows us not to keep the sources and all the development tools provided with the golang image.
Thus, we only keep the binaries.
I decided to use the &lt;strong&gt;golang:1.19-alpine&lt;/strong&gt; image as the &lt;strong&gt;builder&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19-alpine as builder

LABEL stage=gobuilder
ENV CGO_ENABLED=0
ENV GOOS linux
WORKDIR /build

COPY go.mod go.sum .
RUN go mod download

COPY . .
RUN go get -d -v
RUN go build -o /app/myapp .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This first part of the Dockerfile allows us to copy the sources, to synchronize the packages and to launch the command go build to generate the binary.
Once the build is finished, we use a minimalist alpine image that will be used for deployment. We just need to copy the binary from our builder and run it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/myapp .

EXPOSE 8080

CMD [&quot;/app/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After testing this new Dockerfile, the final image was 34MB. It&apos;s far from the original 10MB but it&apos;s still much better than 2GB.&lt;/p&gt;
&lt;h2&gt;Improve the build process&lt;/h2&gt;
&lt;p&gt;It is possible in Golang to add flags to the build command in order to slightly optimize the binary size.
After a little research on internet, I discovered the existence of &quot;-s -w&quot; flags. After testing them locally, I noticed an improvement of a few MB so I decided to add them to my Dockerfile.
I also discovered &lt;a href=&quot;https://upx.github.io/&quot;&gt;Upx&lt;/a&gt; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;&amp;lt;/a&amp;gt; which is a small program that allows to compress binary files to reduce their size.
I tested this little software in a terminal and I noticed an improvement of almost &amp;lt;b&amp;gt;50%&amp;lt;/b&amp;gt; on the size of the Memnix api. So I also added this step to my Dockerfile.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM golang:1.19-alpine as builder

LABEL stage=gobuilder
ENV CGO_ENABLED=0
ENV GOOS linux
RUN apk update --no-cache &amp;amp;&amp;amp; apk add upx
WORKDIR /build

COPY go.mod go.sum .
RUN go mod download

COPY . .
RUN go get -d -v
RUN go build -ldflags=&quot;-s -w&quot;  -o /app/myapp .
RUN upx /app/myapp

FROM alpine:3.14

WORKDIR /app

COPY --from=builder /app/myapp .

EXPOSE 8080

CMD [&quot;/app/myapp&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is my new Dockerfile! It may seem much longer and more complex than the first version but in the end, the result is very interesting. After testing this new Dockerfile, the final image is 16Mb so only 6Mb more than the version without Docker which is almost negligible.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Docker is a great tool to simplify application development and deployment, but it can be problematic if used incorrectly. This experience helped me understand how to better use Docker and how to optimize my images.&lt;/p&gt;
&lt;h2&gt;Related Articles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/blog/optimizing-goroutines-sum-of-squares/&quot;&gt;Solving the Sum of Squares Problem: Optimizing Performance&lt;/a&gt;
This article is inspired by a &amp;lt;a href=&quot;https://twitter.com/GSCorentinDev/status/1564536030795075585?s=20&amp;amp;t=l2hgvKBPDMVs4pS6um2HeA&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;twitter thread&amp;lt;/a&amp;gt; and the complete code of my Dockerfile is available on my &amp;lt;a href=&quot;https://github.com/memnix/memnix-rest/blob/e8e52b3d10731df5b2767f0d24bcdcb5d13d72e3/Dockerfile&quot; style=&quot;font-weight: 700; text-decoration: underline; color: var(--primary);&quot;&amp;gt;github&amp;lt;/a&amp;gt;.
I hope this first article will interest you, don&apos;t hesitate to give me feedback so I can improve for the next articles and share your tips on Docker or Golang!&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>